-
Notifications
You must be signed in to change notification settings - Fork 7
Split push for blocking and non-blocking #244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9fa6c04
dd03a38
ff3afac
7081687
87f072e
ed47d58
aab4c7d
a577092
4500d0b
b326fba
5484eee
4a57c86
0b1049f
be9c23b
75059b9
c42bcc7
cd3b55a
2c81a14
51127ab
296de65
dfa8868
e175f0b
513a06a
4bfa65c
d74c7b3
da39507
5ba7535
df1a1c4
bafd30f
e886dfc
81e7909
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import logging | ||
from time import sleep | ||
|
||
import math | ||
import os | ||
import json | ||
|
@@ -31,9 +33,17 @@ | |
download_project_finalize, | ||
download_project_wait, | ||
download_diffs_finalize, | ||
pull_project_async, | ||
pull_project_wait, | ||
pull_project_finalize, | ||
) | ||
from .client_push import ( | ||
push_project_wait, | ||
push_project_finalize, | ||
push_project_async, | ||
push_project_is_running, | ||
get_change_batch, | ||
) | ||
from .client_pull import pull_project_async, pull_project_wait, pull_project_finalize | ||
from .client_push import push_project_async, push_project_wait, push_project_finalize | ||
from .utils import DateTimeEncoder, get_versions_with_file_changes, int_version, is_version_acceptable | ||
from .version import __version__ | ||
|
||
|
@@ -45,6 +55,10 @@ class TokenError(Exception): | |
pass | ||
|
||
|
||
class AuthTokenExpiredError(Exception): | ||
pass | ||
|
||
|
||
class ServerType(Enum): | ||
OLD = auto() # Server is old and does not support workspaces | ||
CE = auto() # Server is Community Edition | ||
|
@@ -80,8 +94,16 @@ class MerginClient: | |
Currently, only HTTP proxies are supported. | ||
""" | ||
|
||
def __init__(self, url=None, auth_token=None, login=None, password=None, plugin_version=None, proxy_config=None): | ||
self.url = url if url is not None else MerginClient.default_url() | ||
def __init__( | ||
self, | ||
url=None, | ||
auth_token=None, | ||
login=None, | ||
password=None, | ||
plugin_version=None, | ||
proxy_config=None, | ||
): | ||
self.url = (url if url is not None else MerginClient.default_url()).rstrip("/") + "/" | ||
self._auth_params = None | ||
self._auth_session = None | ||
self._user_info = None | ||
|
@@ -91,6 +113,8 @@ def __init__(self, url=None, auth_token=None, login=None, password=None, plugin_ | |
if plugin_version is not None: # this could be e.g. "Plugin/2020.1 QGIS/3.14" | ||
self.client_version += " " + plugin_version | ||
self.setup_logging() | ||
self.pull_job = None | ||
self.push_job = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think these need to be member variables of MerginClient There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's because I want to have an option to cancel the job on keyboard interruption in CLI. I need an instance that is aware of the jobs. |
||
if auth_token: | ||
try: | ||
token_data = decode_token_data(auth_token) | ||
|
@@ -190,11 +214,19 @@ def wrapper(self, *args): | |
delta = self._auth_session["expire"] - datetime.now(timezone.utc) | ||
if delta.total_seconds() < 5: | ||
self.log.info("Token has expired - refreshing...") | ||
self.login(self._auth_params["login"], self._auth_params["password"]) | ||
if self._auth_params.get("login", None) and self._auth_params.get("password", None): | ||
self.log.info("Token has expired - refreshing...") | ||
self.login(self._auth_params["login"], self._auth_params["password"]) | ||
else: | ||
raise AuthTokenExpiredError("Token has expired - please re-login") | ||
else: | ||
# Create a new authorization token | ||
self.log.info(f"No token - login user: {self._auth_params['login']}") | ||
self.login(self._auth_params["login"], self._auth_params["password"]) | ||
if self._auth_params.get("login", None) and self._auth_params.get("password", None): | ||
self.login(self._auth_params["login"], self._auth_params["password"]) | ||
else: | ||
raise ClientError("Missing login or password") | ||
|
||
return f(self, *args) | ||
|
||
return wrapper | ||
|
@@ -453,7 +485,8 @@ def create_project(self, project_name, is_public=False, namespace=None): | |
|
||
def create_project_and_push(self, project_name, directory, is_public=False, namespace=None): | ||
""" | ||
Convenience method to create project and push the initial version right after that. | ||
Convenience method to create project and push the the files right after that. | ||
Creates two versions when directory contains blocking and non-blocking changes. | ||
|
||
:param project_name: Project's full name (<namespace>/<name>) | ||
:type project_name: String | ||
|
@@ -877,7 +910,7 @@ def push_project(self, directory): | |
:type directory: String | ||
""" | ||
job = push_project_async(self, directory) | ||
if job is None: | ||
if not job: | ||
return # there is nothing to push (or we only deleted some files) | ||
push_project_wait(job) | ||
push_project_finalize(job) | ||
|
@@ -895,6 +928,62 @@ def pull_project(self, directory): | |
pull_project_wait(job) | ||
return pull_project_finalize(job) | ||
|
||
def sync_project(self, project_dir): | ||
""" | ||
Syncs project by loop with these steps: | ||
1. Pull server version | ||
2. Get local changes | ||
3. Push first change batch | ||
Repeat if there are more local changes. | ||
The batch pushing makes use of the server ability to handle simultaneously exclusive upload (that blocks | ||
other uploads) and non-exclusive upload (for adding assets) | ||
""" | ||
has_more_changes = True | ||
while has_more_changes: | ||
self.pull_job = pull_project_async(self, project_dir) | ||
if self.pull_job is not None: | ||
pull_project_wait(self.pull_job) | ||
pull_project_finalize(self.pull_job) | ||
|
||
changes_batch, has_more_changes = get_change_batch(self, project_dir) | ||
if not changes_batch: # no changes to upload, quit sync | ||
return | ||
|
||
self.push_job = push_project_async(self, project_dir, changes_batch) | ||
if self.push_job: | ||
push_project_wait(self.push_job) | ||
push_project_finalize(self.push_job) | ||
|
||
def sync_project_with_callback(self, project_dir, progress_callback=None, sleep_time=0.1): | ||
""" | ||
Syncs project while sending push progress info as callback. | ||
Sync is done in this loop: | ||
Pending changes? -> Pull -> Get changes batch -> Push the changes -> repeat | ||
|
||
:param progress_callback: updates the progress bar in CLI, on_progress(increment) | ||
:param sleep_time: sleep time between calling the callback function | ||
""" | ||
has_more_changes = True | ||
while has_more_changes: | ||
self.pull_job = pull_project_async(self, project_dir) | ||
if self.pull_job is not None: | ||
pull_project_wait(self.pull_job) | ||
pull_project_finalize(self.pull_job) | ||
|
||
changes_batch, has_more_changes = get_change_batch(self, project_dir) | ||
if not changes_batch: # no changes to upload, quit sync | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it correct to have the early return there? I guess if there are multiple batches, in theory there could be a single batch with just file removals (thus not uploading any data), and later some batches with data - and this early return would skip the other batches |
||
|
||
self.push_job = push_project_async(self, project_dir, changes_batch) | ||
if self.push_job: | ||
last = 0 | ||
while push_project_is_running(self.push_job): | ||
sleep(sleep_time) | ||
now = self.push_job.transferred_size | ||
progress_callback(now - last) # update progressbar with transferred size increment | ||
last = now | ||
push_project_finalize(self.push_job) | ||
|
||
def clone_project(self, source_project_path, cloned_project_name, cloned_project_namespace=None): | ||
""" | ||
Clone project on server. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this change - it does not seem to be relevant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not relevant with the rest - shall be moved to a separate PR?
I just hit this when didn't include the trailing slash to localhost url