Skip to content

Commit

Permalink
Use explicit function calls to get project metadata
Browse files Browse the repository at this point in the history
This is to make the code more clear about what data it gets from metadata,
and also to be ready for future when we change the way how metadata are stored.
  • Loading branch information
wonder-sk committed Oct 9, 2023
1 parent c90d284 commit 5614f54
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 53 deletions.
6 changes: 3 additions & 3 deletions mergin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ def show_version(ctx, version):
return
directory = os.getcwd()
mp = MerginProject(directory)
project_path = mp.metadata["name"]
project_path = mp.project_full_name()
# TODO: handle exception when version not found
version_info_dict = mc.project_version_info(project_path, version)[0]
click.secho("Project: " + version_info_dict["namespace"] + "/" + version_info_dict["project_name"])
Expand All @@ -514,7 +514,7 @@ def show_file_history(ctx, path):
return
directory = os.getcwd()
mp = MerginProject(directory)
project_path = mp.metadata["name"]
project_path = mp.project_full_name()
info_dict = mc.project_file_history_info(project_path, path)
# TODO: handle exception if history not found
history_dict = info_dict["history"]
Expand All @@ -538,7 +538,7 @@ def show_file_changeset(ctx, path, version):
return
directory = os.getcwd()
mp = MerginProject(directory)
project_path = mp.metadata["name"]
project_path = mp.project_full_name()
info_dict = mc.project_file_changeset_info(project_path, path, version)
# TODO: handle exception if Diff not found
click.secho(json.dumps(info_dict, indent=2))
Expand Down
16 changes: 11 additions & 5 deletions mergin/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,16 @@ def create_project_and_push(self, project_name, directory, is_public=False, name
if directory:
full_project_name = "{}/{}".format(namespace, project_name)
project_info = self.project_info(full_project_name)
MerginProject.write_metadata(
directory,
{
"name": full_project_name,
"version": "v0",
"files": [],
"project_id": project_info["id"],
},
)
mp = MerginProject(directory)
mp.metadata = {"name": full_project_name, "version": "v0", "files": [], "project_id": project_info["id"]}
if mp.inspect_files():
self.push_project(directory)

Expand Down Expand Up @@ -878,9 +886,7 @@ def project_status(self, directory):
:rtype: dict, dict
"""
mp = MerginProject(directory)
project_path = mp.metadata["name"]
local_version = mp.metadata["version"]
server_info = self.project_info(project_path, since=local_version)
server_info = self.project_info(mp.project_full_name(), since=mp.version())

pull_changes = mp.get_pull_changes(server_info["files"])

Expand Down Expand Up @@ -955,7 +961,7 @@ def get_file_diff(self, project_dir, file_path, output_diff, version_from, versi
:type version_to: String
"""
mp = MerginProject(project_dir)
project_path = mp.metadata["name"]
project_path = mp.project_full_name()
file_history = self.project_file_history_info(project_path, file_path)
versions_to_fetch = get_versions_with_file_changes(
self, project_path, file_path, version_from=version_from, version_to=version_to, file_history=file_history
Expand Down
38 changes: 21 additions & 17 deletions mergin/client_pull.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,14 @@ def download_project_finalize(job):

# final update of project metadata
# TODO: why not exact copy of project info JSON ?
job.mp.metadata = {
"name": job.project_path,
"version": job.version,
"project_id": job.project_info["id"],
"files": job.project_info["files"],
}
job.mp.update_metadata(
{
"name": job.project_path,
"version": job.version,
"project_id": job.project_info["id"],
"files": job.project_info["files"],
}
)


def download_project_cancel(job):
Expand Down Expand Up @@ -370,8 +372,8 @@ def pull_project_async(mc, directory):
mp.log.info("--- pull aborted")
raise

project_path = mp.metadata["name"]
local_version = mp.metadata["version"]
project_path = mp.project_full_name()
local_version = mp.version()

mp.log.info("--- version: " + mc.user_agent_info())
mp.log.info(f"--- start pull {project_path}")
Expand Down Expand Up @@ -611,17 +613,19 @@ def pull_project_finalize(job):
job.mp.log.info("--- pull aborted")
raise ClientError("Failed to apply pull changes: " + str(e))

job.mp.metadata = {
"name": job.project_path,
"version": job.version if job.version else "v0", # for new projects server version is ""
"project_id": job.project_info["id"],
"files": job.project_info["files"],
}
job.mp.update_metadata(
{
"name": job.project_path,
"version": job.version if job.version else "v0", # for new projects server version is ""
"project_id": job.project_info["id"],
"files": job.project_info["files"],
}
)

if job.mp.has_unfinished_pull():
job.mp.log.info("--- failed to complete pull -- project left in the unfinished pull state")
else:
job.mp.log.info("--- pull finished -- at version " + job.mp.metadata["version"])
job.mp.log.info("--- pull finished -- at version " + job.mp.version())

shutil.rmtree(job.temp_dir)
return conflicts
Expand All @@ -633,7 +637,7 @@ def download_file_async(mc, project_dir, file_path, output_file, version):
Returns handle to the pending download.
"""
mp = MerginProject(project_dir)
project_path = mp.metadata["name"]
project_path = mp.project_full_name()
ver_info = f"at version {version}" if version is not None else "at latest version"
mp.log.info(f"Getting {file_path} {ver_info}")
latest_proj_info = mc.project_info(project_path)
Expand Down Expand Up @@ -720,7 +724,7 @@ def download_diffs_async(mc, project_directory, file_path, versions):
PullJob/None: a handle for the pending download.
"""
mp = MerginProject(project_directory)
project_path = mp.metadata["name"]
project_path = mp.project_full_name()
file_history = mc.project_file_history_info(project_path, file_path)
mp.log.info(f"--- version: {mc.user_agent_info()}")
mp.log.info(f"--- start download diffs for {file_path} of {project_path}, versions: {[v for v in versions]}")
Expand Down
18 changes: 10 additions & 8 deletions mergin/client_push.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ def push_project_async(mc, directory):
if mp.has_unfinished_pull():
raise ClientError("Project is in unfinished pull state. Please resolve unfinished pull and try again.")

project_path = mp.metadata["name"]
local_version = mp.metadata["version"]
project_path = mp.project_full_name()
local_version = mp.version()

mp.log.info("--- version: " + mc.user_agent_info())
mp.log.info(f"--- start push {project_path}")
Expand Down Expand Up @@ -272,12 +272,14 @@ def push_project_finalize(job):
job.mp.log.info("cancel response: " + str(err2))
raise err

job.mp.metadata = {
"name": job.project_path,
"version": job.server_resp["version"],
"project_id": job.server_resp["id"],
"files": job.server_resp["files"],
}
job.mp.update_metadata(
{
"name": job.project_path,
"version": job.server_resp["version"],
"project_id": job.server_resp["id"],
"files": job.server_resp["files"],
}
)
try:
job.mp.apply_push_changes(job.changes)
except Exception as e:
Expand Down
93 changes: 82 additions & 11 deletions mergin/merginproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def __init__(self, directory):
if not os.path.exists(self.cache_dir):
os.mkdir(self.cache_dir)

# metadata from JSON are lazy loaded
self._metadata = None

self.setup_logging(directory)

# make sure we can load correct pygeodiff
Expand Down Expand Up @@ -132,16 +135,84 @@ def fpath_cache(self, file, version=None):
return self.fpath(file, os.path.join(self.cache_dir, version))
return self.fpath(file, self.cache_dir)

def project_full_name(self) -> str:
""" Returns fully qualified project name: <workspace>/<name> """
if self._metadata is None:
self._read_metadata()
return self._metadata["name"]

def project_name(self) -> str:
""" Returns only project name, without its workspace name """
full_name = self.project_full_name()
slash_index = full_name.index("/")
return full_name[slash_index+1:]

def workspace_name(self) -> str:
""" Returns name of the workspace where the project belongs """
full_name = self.project_full_name()
slash_index = full_name.index("/")
return full_name[:slash_index]

def project_id(self) -> str:
""" Returns ID of the project (UUID using 8-4-4-4-12 formatting without braces) """
if self._metadata is None:
self._read_metadata()
return self._metadata["project_id"]

def workspace_id(self) -> int:
""" Returns ID of the workspace where the project belongs """
# unfortunately we currently do not have information about workspace ID
# in project's metadata...
raise NotImplementedError

def version(self) -> str:
""" Returns project version (e.g. "v123") """
if self._metadata is None:
self._read_metadata()
return self._metadata["version"]

def files(self) -> list:
""" Returns project's list of files (each file being a dictionary) """
if self._metadata is None:
self._read_metadata()
return self._metadata["files"]

@property
def metadata(self):
def metadata(self) -> dict:
""" Gets raw access to metadata. Kept only for backwards compatibility and will be removed. """
# as we will change what is written in mergin.json, we do not want
# client code to use this getter, and rather use project_full_name(), version() etc.
from warnings import warn
warn("MerginProject.metadata getter should not be used anymore", DeprecationWarning)
if self._metadata is None:
self._read_metadata()
return self._metadata

def _read_metadata(self):
""" Loads the project's metadata from JSON """
if not os.path.exists(self.fpath_meta("mergin.json")):
raise InvalidProject("Project metadata has not been created yet")
with open(self.fpath_meta("mergin.json"), "r") as file:
return json.load(file)

@metadata.setter
def metadata(self, data):
with open(self.fpath_meta("mergin.json"), "w") as file:
self._metadata = json.load(file)

def update_metadata(self, data: dict):
""" Writes project metadata and updates cached metadata. """
self._metadata = data
MerginProject.write_metadata(self.dir, data)

@staticmethod
def write_metadata(project_directory: str, data: dict):
""" Writes project metadata to <project_directory>/.mergin/mergin.json
In most cases it is better to call update_metadata() as that will also
update in-memory cache of metadata in MerginProject - this static method is
useful for cases when this is the first time metadata are being written
(and therefore creating MerginProject would fail).
"""
meta_dir = os.path.join(project_directory, ".mergin")
os.makedirs(meta_dir, exist_ok=True)
metadata_json_file = os.path.abspath(os.path.join(meta_dir, "mergin.json"))
with open(metadata_json_file, "w") as file:
json.dump(data, file, indent=2)

def is_versioned_file(self, file):
Expand Down Expand Up @@ -270,7 +341,7 @@ def get_pull_changes(self, server_files):
"""

# first let's have a look at the added/updated/removed files
changes = self.compare_file_sets(self.metadata["files"], server_files)
changes = self.compare_file_sets(self.files(), server_files)

# then let's inspect our versioned files (geopackages) if there are any relevant changes
not_updated = []
Expand All @@ -290,7 +361,7 @@ def get_pull_changes(self, server_files):

# need to track geodiff file history to see if there were any changes
for version, version_info in history_list:
if version <= int_version(self.metadata["version"]):
if version <= int_version(self.version()):
continue # ignore history of no interest
is_updated = True
if "diff" in version_info:
Expand Down Expand Up @@ -321,7 +392,7 @@ def get_push_changes(self):
:returns: changes metadata for files to be pushed to server
:rtype: dict
"""
changes = self.compare_file_sets(self.metadata["files"], self.inspect_files())
changes = self.compare_file_sets(self.files(), self.inspect_files())
# do checkpoint to push changes from wal file to gpkg
for file in changes["added"] + changes["updated"]:
size, checksum = do_sqlite_checkpoint(self.fpath(file["path"]), self.log)
Expand Down Expand Up @@ -514,7 +585,7 @@ def update_with_rebase(self, path, src, dest, basefile, temp_dir, user_name):
# they will be stored in a JSON file - if there are no conflicts, the file
# won't even be created
rebase_conflicts = unique_path_name(
edit_conflict_file_name(self.fpath(path), user_name, int_version(self.metadata["version"]))
edit_conflict_file_name(self.fpath(path), user_name, int_version(self.version()))
)

# try to do rebase magic
Expand Down Expand Up @@ -628,7 +699,7 @@ def create_conflicted_copy(self, file, user_name):
return

backup_path = unique_path_name(
conflicted_copy_file_name(self.fpath(file), user_name, int_version(self.metadata["version"]))
conflicted_copy_file_name(self.fpath(file), user_name, int_version(self.version()))
)

if self.is_versioned_file(file):
Expand Down
2 changes: 1 addition & 1 deletion mergin/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def create_report(mc, directory, since, to, out_file):
List of warnings/issues for versions which could not be processed (e.g. broken history with missing diff)
"""
mp = MerginProject(directory)
project = mp.metadata["name"]
project = mp.project_full_name()
mp.log.info(f"--- Creating changesets report for {project} from {since} to {to if to else 'latest'} versions ----")
versions = mc.project_versions(project, since, to if to else None)
versions_map = {v["name"]: v for v in versions}
Expand Down
Loading

0 comments on commit 5614f54

Please sign in to comment.