Merge branch 'develop' into feature/nuke_in_pype3

This commit is contained in:
Jakub Jezek 2021-01-20 17:35:56 +01:00
commit e9457ad8db
No known key found for this signature in database
GPG key ID: C4B96E101D2A47F3
189 changed files with 5766 additions and 764 deletions

7
.gitmodules vendored
View file

@ -2,19 +2,12 @@
path = repos/avalon-core
url = git@github.com:pypeclub/avalon-core.git
branch = develop
[submodule "repos/pyblish-base"]
path = repos/pyblish-base
url = git@github.com:pyblish/pyblish-base.git
[submodule "repos/avalon-unreal-integration"]
path = repos/avalon-unreal-integration
url = git@github.com:pypeclub/avalon-unreal-integration.git
[submodule "repos/maya-look-assigner"]
path = repos/maya-look-assigner
url = git@github.com:pypeclub/maya-look-assigner.git
[submodule "repos/acre"]
path = repos/acre
url = git@github.com:antirotor/acre.git
branch = fix/unformatted-tokens
[submodule "pype/modules/ftrack/python2_vendor/ftrack-python-api"]
path = pype/modules/ftrack/python2_vendor/ftrack-python-api
url = https://bitbucket.org/ftrack/ftrack-python-api.git

View file

@ -1,33 +1,106 @@
# Pype
## Introduction
Pype
====
Multi-platform open-source pipeline built around the [Avalon](https://getavalon.github.io/) platform, expanding it with extra features and integrations. Pype connects asset database, project management and time tracking into a single modular system. It has tight integration with [ftrack](https://www.ftrack.com/en/), but it can also run independently.
Introduction
------------
Multi-platform open-source pipeline built around the [Avalon](https://getavalon.github.io/) platform,
expanding it with extra features and integrations. Pype connects asset database, project management
and time tracking into a single modular system. It has tight integration
with [ftrack](https://www.ftrack.com/en/), but it can also run independently.
To get all the key information about the project, go to [PYPE.club](http://pype.club)
## Hardware requirements
Requirements
------------
Pype will run on most typical hardware configurations commonly found in studios around the world.
It is installed on artist computer and can take up 3Gb of space depending on number of versions
and other dependencies.
Pype should be installed centrally on a fast network storage with at least read access right for all workstations and users in the Studio. Full Deplyoyment with all dependencies and both Development and Production branches installed takes about 1GB of data, however to ensure smooth updates and general working comfort, we recommend allocating at least at least 4GB of storage dedicated to PYPE deployment.
For well functioning [ftrack](https://www.ftrack.com/en/) event server, we recommend a
linux virtual server with [Ubuntu](https://ubuntu.com/) or [CentosOS](https://www.centos.org/).
CPU and RAM allocation need differ based on the studio size, but a 2GB of RAM, with a
dual core CPU and around 4GB of storage should suffice.
For well functioning [ftrack](https://www.ftrack.com/en/) event server, we recommend a linux virtual server with [Ubuntu](https://ubuntu.com/) or [CentosOS](https://www.centos.org/). CPU and RAM allocation need differ based on the studio size, but a 2GB of RAM, with a dual core CPU and around 4GB of storage should suffice.
Pype needs running [mongodb](https://www.mongodb.com/) server with good connectivity as it is
heavily used by Pype. Depending on project size and number of artists working connection speed and
latency influence performance experienced by artists. If remote working is required, this mongodb
server must be accessible from Internet or cloud solution can be used. Reasonable backup plan
or high availability options are recommended.
## Building Pype
Building Pype
-------------
### Windows
You will need [Python 3.7 and newer](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads).
More tools might be needed for installing dependencies (for example for **OpenTimelineIO**) - mostly
development tools like [CMake](https://cmake.org/) and [Visual Studio](https://visualstudio.microsoft.com/cs/downloads/)
Clone repository:
```sh
git clone --recurse-submodules git@github.com:pypeclub/pype.git
```
Run PowerShell script `build.ps1`. It will create *venv*, install all
required dependencies and build Pype. After it is finished, you will find
Pype in `build` folder.
#### To build Pype:
You might need more tools for installing dependencies (for example for **OpenTimelineIO**) - mostly
development tools like [CMake](https://cmake.org/) and [Visual Studio](https://visualstudio.microsoft.com/cs/downloads/)
1) Run `.\tools\create_env.ps1` to create virtual environment in `.\venv`
2) Run `.\tools\build.ps1` to build pype executables in `.\build\`
To create distributable Pype versions, run `./tools/create_zip.ps1` - that will
create zip file with name `pype-vx.x.x.zip` parsed from current pype repository and
copy it to user data dir, or you can specify `--path /path/to/zip` to force it there.
You can then point **Igniter** - Pype setup tool - to directory containing this zip and
it will install it on current computer.
Pype is build using [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze itself and all dependencies.
Running Pype
------------
Pype can by executed either from live sources (this repository) or from
*"frozen code"* - executables that can be build using steps described above.
If Pype is executed from live sources, it will use Pype version included in them. If
it is executed from frozen code it will try to find latest Pype version installed locally
on current computer and if it is not found, it will ask for its location. On that location
pype can be either in directories or zip files. Pype will try to find latest version and
install it to user data directory (on Windows to `%LOCALAPPDATA%\pypeclub\pype`).
### From sources
Pype can be run directly from sources by activating virtual environment:
```powershell
.\venv\Scripts\Activate.ps1
```
and running:
```powershell
python start.py tray
```
This will use current Pype version with sources. You can override this with `--use-version=x.x.x` and
then Pype will try to find locally installed specified version (present in user data directory).
### From frozen code
You need to build Pype first. This will produce two executables - `pype.exe` and `pype_console.exe`.
First one will act as GUI application and will not create console (useful in production environments).
The second one will create console and will write output there - useful for headless application and
debugging purposes. If you need pype version installed, just run `./tools/create_zip.ps1` without
arguments and it will create zip file that pype can use.
Building documentation
----------------------
Top build API documentation, run `.\tools\make_docs.ps1`. It will create html documentation
from current sources in `.\docs\build`.
**Note that it needs existing virtual environment.**
Running tests
-------------
To run tests, execute `.\tools\run_tests.ps1`.
**Note that it needs existing virtual environment.**

View file

@ -1,2 +0,0 @@
.\venv\Scripts\Activate.ps1
python pype.py mongodb

View file

@ -1,2 +0,0 @@
.\venv\Scripts\Activate.ps1
python pype.py settings --dev

View file

@ -1,2 +0,0 @@
.\venv\Scripts\Activate.ps1
python pype.py tray --debug

View file

@ -0,0 +1,7 @@
pype.hosts.aftereffects package
===============================
.. automodule:: pype.hosts.aftereffects
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.hosts.resolve.otio.davinci\_export module
==============================================
.. automodule:: pype.hosts.resolve.otio.davinci_export
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.hosts.resolve.otio.davinci\_import module
==============================================
.. automodule:: pype.hosts.resolve.otio.davinci_import
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,17 @@
pype.hosts.resolve.otio package
===============================
.. automodule:: pype.hosts.resolve.otio
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
:maxdepth: 10
pype.hosts.resolve.otio.davinci_export
pype.hosts.resolve.otio.davinci_import
pype.hosts.resolve.otio.utils

View file

@ -0,0 +1,7 @@
pype.hosts.resolve.otio.utils module
====================================
.. automodule:: pype.hosts.resolve.otio.utils
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.hosts.resolve.todo\-rendering module
=========================================
.. automodule:: pype.hosts.resolve.todo-rendering
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.hosts.tvpaint.api package
==============================
.. automodule:: pype.hosts.tvpaint.api
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,15 @@
pype.hosts.tvpaint package
==========================
.. automodule:: pype.hosts.tvpaint
:members:
:undoc-members:
:show-inheritance:
Subpackages
-----------
.. toctree::
:maxdepth: 10
pype.hosts.tvpaint.api

View file

@ -0,0 +1,7 @@
pype.lib.abstract\_collect\_render module
=========================================
.. automodule:: pype.lib.abstract_collect_render
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.abstract\_expected\_files module
=========================================
.. automodule:: pype.lib.abstract_expected_files
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.abstract\_metaplugins module
=====================================
.. automodule:: pype.lib.abstract_metaplugins
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.abstract\_submit\_deadline module
==========================================
.. automodule:: pype.lib.abstract_submit_deadline
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.applications module
============================
.. automodule:: pype.lib.applications
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.avalon\_context module
===============================
.. automodule:: pype.lib.avalon_context
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.deprecated module
==========================
.. automodule:: pype.lib.deprecated
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.editorial module
=========================
.. automodule:: pype.lib.editorial
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.env\_tools module
==========================
.. automodule:: pype.lib.env_tools
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.ffmpeg\_utils module
=============================
.. automodule:: pype.lib.ffmpeg_utils
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.path\_tools module
===========================
.. automodule:: pype.lib.path_tools
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.plugin\_tools module
=============================
.. automodule:: pype.lib.plugin_tools
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.python\_module\_tools module
=====================================
.. automodule:: pype.lib.python_module_tools
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.lib.terminal\_splash module
================================
.. automodule:: pype.lib.terminal_splash
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.clockify.clockify\_module module
=============================================
.. automodule:: pype.modules.clockify.clockify_module
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.deadline.deadline\_module module
=============================================
.. automodule:: pype.modules.deadline.deadline_module
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,15 @@
pype.modules.deadline package
=============================
.. automodule:: pype.modules.deadline
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
:maxdepth: 10
pype.modules.deadline.deadline_module

View file

@ -0,0 +1,7 @@
pype.modules.ftrack.ftrack\_module module
=========================================
.. automodule:: pype.modules.ftrack.ftrack_module
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.ftrack.lib.settings module
=======================================
.. automodule:: pype.modules.ftrack.lib.settings
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.ftrack.tray.ftrack\_tray module
============================================
.. automodule:: pype.modules.ftrack.tray.ftrack_tray
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.launcher\_action module
====================================
.. automodule:: pype.modules.launcher_action
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.log\_viewer.log\_view\_module module
=================================================
.. automodule:: pype.modules.log_viewer.log_view_module
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,23 @@
pype.modules.log\_viewer package
================================
.. automodule:: pype.modules.log_viewer
:members:
:undoc-members:
:show-inheritance:
Subpackages
-----------
.. toctree::
:maxdepth: 10
pype.modules.log_viewer.tray
Submodules
----------
.. toctree::
:maxdepth: 10
pype.modules.log_viewer.log_view_module

View file

@ -0,0 +1,7 @@
pype.modules.log\_viewer.tray.app module
========================================
.. automodule:: pype.modules.log_viewer.tray.app
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.log\_viewer.tray.models module
===========================================
.. automodule:: pype.modules.log_viewer.tray.models
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,17 @@
pype.modules.log\_viewer.tray package
=====================================
.. automodule:: pype.modules.log_viewer.tray
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
:maxdepth: 10
pype.modules.log_viewer.tray.app
pype.modules.log_viewer.tray.models
pype.modules.log_viewer.tray.widgets

View file

@ -0,0 +1,7 @@
pype.modules.log\_viewer.tray.widgets module
============================================
.. automodule:: pype.modules.log_viewer.tray.widgets
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.settings\_action module
====================================
.. automodule:: pype.modules.settings_action
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.standalonepublish\_action module
=============================================
.. automodule:: pype.modules.standalonepublish_action
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,16 @@
pype.modules.sync\_server package
=================================
.. automodule:: pype.modules.sync_server
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
:maxdepth: 10
pype.modules.sync_server.sync_server
pype.modules.sync_server.utils

View file

@ -0,0 +1,7 @@
pype.modules.sync\_server.sync\_server module
=============================================
.. automodule:: pype.modules.sync_server.sync_server
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.sync\_server.utils module
======================================
.. automodule:: pype.modules.sync_server.utils
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.modules.websocket\_server.hosts.aftereffects module
========================================================
.. automodule:: pype.modules.websocket_server.hosts.aftereffects
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.plugins.maya.publish.validate\_vray\_referenced\_aovs module
=================================================================
.. automodule:: pype.plugins.maya.publish.validate_vray_referenced_aovs
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.settings.constants module
==============================
.. automodule:: pype.settings.constants
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.settings.handlers module
=============================
.. automodule:: pype.settings.handlers
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.tests.test\_lib\_restructuralization module
================================================
.. automodule:: pype.tests.test_lib_restructuralization
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.tools.tray.pype\_tray module
=================================
.. automodule:: pype.tools.tray.pype_tray
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,15 @@
pype.tools.tray package
=======================
.. automodule:: pype.tools.tray
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
:maxdepth: 10
pype.tools.tray.pype_tray

View file

@ -0,0 +1,7 @@
pype.tools.workfiles.app module
===============================
.. automodule:: pype.tools.workfiles.app
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
pype.tools.workfiles.model module
=================================
.. automodule:: pype.tools.workfiles.model
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,17 @@
pype.tools.workfiles package
============================
.. automodule:: pype.tools.workfiles
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
.. toctree::
:maxdepth: 10
pype.tools.workfiles.app
pype.tools.workfiles.model
pype.tools.workfiles.view

View file

@ -0,0 +1,7 @@
pype.tools.workfiles.view module
================================
.. automodule:: pype.tools.workfiles.view
:members:
:undoc-members:
:show-inheritance:

View file

@ -9,8 +9,10 @@ from .bootstrap_repos import BootstrapRepos
def run():
"""Show Igniter dialog."""
app = QtWidgets.QApplication(sys.argv)
d = InstallDialog()
d.exec_()
d.show()
sys.exit(app.exec_())

View file

@ -1,21 +1,20 @@
# -*- coding: utf-8 -*-
"""Bootstrap Pype repositories."""
import sys
import functools
import logging as log
import os
import re
import logging as log
import shutil
import sys
import tempfile
from typing import Union, Callable, List
from zipfile import ZipFile
from pathlib import Path
import functools
from speedcopy import copyfile
from typing import Union, Callable, List, Tuple
from zipfile import ZipFile, BadZipFile
from appdirs import user_data_dir
from pype.version import __version__
from pype.lib import PypeSettingsRegistry
from speedcopy import copyfile
from .user_settings import PypeSettingsRegistry
from .tools import load_environments
@ -24,21 +23,24 @@ class PypeVersion:
"""Class for storing information about Pype version.
Attributes:
major (int): [1].2.3-variant-client
minor (int): 1.[2].3-variant-client
subversion (int): 1.2.[3]-variant-client
variant (str): 1.2.3-[variant]-client
client (str): 1.2.3-variant-[client]
major (int): [1].2.3-client-variant
minor (int): 1.[2].3-client-variant
subversion (int): 1.2.[3]-client-variant
client (str): 1.2.3-[client]-variant
variant (str): 1.2.3-client-[variant]
path (str): path to Pype
"""
major = 0
minor = 0
subversion = 0
variant = "production"
variant = ""
client = None
path = None
_version_regex = re.compile(
r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<sub>\d+)(-(?P<var1>staging)|-(?P<client>.+)(-(?P<var2>staging)))?") # noqa: E501
@property
def version(self):
"""return formatted version string."""
@ -55,15 +57,14 @@ class PypeVersion:
def __init__(self, major: int = None, minor: int = None,
subversion: int = None, version: str = None,
variant: str = "production", client: str = None,
variant: str = "", client: str = None,
path: Path = None):
self.path = path
self._version_regex = re.compile(
r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<sub>\d+)(-?((?P<variant>staging)|(?P<client>.+))(-(?P<cli>.+))?)?") # noqa: E501
if major is None or minor is None or subversion is None:
if version is None:
raise ValueError("Need version specified in some way.")
if (
major is None or minor is None or subversion is None
) and version is None:
raise ValueError("Need version specified in some way.")
if version:
values = self._decompose_version(version)
self.major = values[0]
@ -83,25 +84,27 @@ class PypeVersion:
def _compose_version(self):
version = "{}.{}.{}".format(self.major, self.minor, self.subversion)
if self.variant == "staging":
version = "{}-{}".format(version, self.variant)
if self.client:
version = "{}-{}".format(version, self.client)
if self.variant == "staging":
version = "{}-{}".format(version, self.variant)
return version
def _decompose_version(self, version_string: str) -> tuple:
m = re.match(self._version_regex, version_string)
@classmethod
def _decompose_version(cls, version_string: str) -> tuple:
m = re.search(cls._version_regex, version_string)
if not m:
raise ValueError(
"Cannot parse version string: {}".format(version_string))
variant = None
if m.group("variant") == "staging":
if m.group("var1") == "staging" or m.group("var2") == "staging":
variant = "staging"
client = m.group("client") or m.group("cli")
client = m.group("client")
return (int(m.group("major")), int(m.group("minor")),
int(m.group("sub")), variant, client)
@ -122,21 +125,85 @@ class PypeVersion:
return hash(self.version)
def __lt__(self, other):
if self.major < other.major:
if (self.major, self.minor, self.subversion) < \
(other.major, other.minor, other.subversion):
return True
if self.major <= other.major and self.minor < other.minor:
return True
if self.major <= other.major and self.minor <= other.minor and \
self.subversion < other.subversion:
# 1.2.3-staging < 1.2.3-client-staging
if self.get_main_version() == other.get_main_version() and \
not self.client and self.variant and \
other.client and other.variant:
return True
if self.major == other.major and self.minor == other.minor and \
self.subversion == other.subversion and \
self.variant == "staging":
# 1.2.3 < 1.2.3-staging
if self.get_main_version() == other.get_main_version() and \
not self.client and self.variant and \
not other.client and not other.variant:
return True
return False
# 1.2.3 < 1.2.3-client
if self.get_main_version() == other.get_main_version() and \
not self.client and not self.variant and \
other.client and not other.variant:
return True
# 1.2.3 < 1.2.3-client-staging
if self.get_main_version() == other.get_main_version() and \
not self.client and not self.variant and other.client:
return True
# 1.2.3-client-staging < 1.2.3-client
if self.get_main_version() == other.get_main_version() and \
self.client and self.variant and \
other.client and not other.variant:
return True
# prefer path over no path
if self.version == other.version and \
not self.path and other.path:
return True
# prefer path with dir over path with file
return self.version == other.version and self.path and \
other.path and self.path.is_file() and \
other.path.is_dir()
def is_staging(self) -> bool:
"""Test if current version is staging one."""
return self.variant == "staging"
def get_main_version(self) -> str:
"""Return main version component.
This returns x.x.x part of version from possibly more complex one
like x.x.x-foo-bar.
Returns:
str: main version component
"""
return "{}.{}.{}".format(self.major, self.minor, self.subversion)
@staticmethod
def version_in_str(string: str) -> Tuple:
"""Find Pype version in given string.
Args:
string (str): string to search.
Returns:
tuple: True/False and PypeVersion if found.
"""
try:
result = PypeVersion._decompose_version(string)
except ValueError:
return False, None
return True, PypeVersion(major=result[0],
minor=result[1],
subversion=result[2],
variant=result[3],
client=result[4])
class BootstrapRepos:
@ -146,15 +213,20 @@ class BootstrapRepos:
data_dir (Path): local Pype installation directory.
live_repo_dir (Path): path to repos directory if running live,
otherwise `None`.
registry (PypeSettingsRegistry): Pype registry object.
zip_filter (list): List of files to exclude from zip
pype_filter (list): list of top level directories not to include in
zip in Pype repository.
"""
def __init__(self, progress_callback: Callable = None):
def __init__(self, progress_callback: Callable = None, message=None):
"""Constructor.
Args:
progress_callback (callable): Optional callback method to report
progress.
message (QtCore.Signal, optional): Signal to report messages back.
"""
# vendor and app used to construct user data dir
@ -163,9 +235,15 @@ class BootstrapRepos:
self._log = log.getLogger(str(__class__))
self.data_dir = Path(user_data_dir(self._app, self._vendor))
self.registry = PypeSettingsRegistry()
self.zip_filter = [".pyc", "__pycache__"]
self.pype_filter = [
"build", "docs", "tests", "repos", "tools", "venv"
]
self._message = message
# dummy progress reporter
def empty_progress(x: int):
"""Progress callback dummy."""
return x
if not progress_callback:
@ -194,9 +272,14 @@ class BootstrapRepos:
return v.path
@staticmethod
def get_local_version() -> str:
def get_local_live_version() -> str:
"""Get version of local Pype."""
return __version__
version = {}
path = Path(os.path.dirname(__file__)).parent / "pype" / "version.py"
with open(path, "r") as fp:
exec(fp.read(), version)
return version["__version__"]
@staticmethod
def get_version(repo_dir: Path) -> Union[str, None]:
@ -225,7 +308,7 @@ class BootstrapRepos:
"""Copy zip created from Pype repositories to user data dir.
This detect Pype version either in local "live" Pype repository
or in user provided path. Then it will zip in in temporary directory
or in user provided path. Then it will zip it in temporary directory
and finally it will move it to destination which is user data
directory. Existing files will be replaced.
@ -240,7 +323,7 @@ class BootstrapRepos:
# version and use it as a source. Otherwise repo_dir is user
# entered location.
if not repo_dir:
version = self.get_local_version()
version = self.get_local_live_version()
repo_dir = self.live_repo_dir
else:
version = self.get_version(repo_dir)
@ -252,7 +335,7 @@ class BootstrapRepos:
# create zip inside temporary directory.
with tempfile.TemporaryDirectory() as temp_dir:
temp_zip = \
Path(temp_dir) / f"pype-repositories-v{version}.zip"
Path(temp_dir) / f"pype-v{version}.zip"
self._log.info(f"creating zip: {temp_zip}")
self._create_pype_zip(temp_zip, repo_dir)
@ -275,7 +358,7 @@ class BootstrapRepos:
except shutil.Error as e:
self._log.error(e)
return None
return self.data_dir / temp_zip.name
return destination
def _create_pype_zip(
self,
@ -284,11 +367,9 @@ class BootstrapRepos:
"""Pack repositories and Pype into zip.
We are using :mod:`zipfile` instead :meth:`shutil.make_archive`
to later implement file filter to skip git related stuff to make
it into archive.
Todo:
Implement file filter
because we need to decide what file and directories to include in zip
and what not. They are determined by :attr:`zip_filter` on file level
and :attr:`pype_filter` on top level directory in Pype repository.
Args:
zip_path (str): path to zip file.
@ -296,42 +377,82 @@ class BootstrapRepos:
include_pype (bool): add Pype module itself.
"""
repo_files = sum(len(files) for _, _, files in os.walk(include_dir))
include_dir = include_dir.resolve()
def _filter_dir(path: Path, path_filter: List) -> List[Path]:
"""Recursively crawl over path and filter."""
result = []
for item in path.iterdir():
if item.name in path_filter:
continue
if item.name.startswith('.'):
continue
if item.is_dir():
result.extend(_filter_dir(item, path_filter))
else:
result.append(item)
return result
pype_list = []
# get filtered list of files in repositories (repos directory)
repo_list = _filter_dir(include_dir, self.zip_filter)
# count them
repo_files = len(repo_list)
# there must be some files, otherwise `include_dir` path is wrong
assert repo_files != 0, f"No repositories to include in {include_dir}"
pype_inc = 0
if include_pype:
pype_files = sum(len(files) for _, _, files in os.walk(
include_dir.parent))
# get filtered list of file in Pype repository
pype_list = _filter_dir(include_dir.parent, self.zip_filter)
pype_files = len(pype_list)
repo_inc = 48.0 / float(repo_files)
pype_inc = 48.0 / float(pype_files)
else:
repo_inc = 98.0 / float(repo_files)
progress = 0
with ZipFile(zip_path, "w") as zip_file:
for root, _, files in os.walk(include_dir.as_posix()):
for file in files:
zip_file.write(
os.path.relpath(os.path.join(root, file),
os.path.join(include_dir, '..')),
os.path.relpath(os.path.join(root, file),
os.path.join(include_dir))
)
progress += repo_inc
self._progress_callback(int(progress))
file: Path
for file in repo_list:
progress += repo_inc
self._progress_callback(int(progress))
# archive name is relative to repos dir
arc_name = file.relative_to(include_dir)
zip_file.write(file, arc_name)
# add pype itself
if include_pype:
for root, _, files in os.walk("pype"):
for file in files:
zip_file.write(
os.path.relpath(os.path.join(root, file),
os.path.join('pype', '..')),
os.path.join(
'pype',
os.path.relpath(os.path.join(root, file),
os.path.join('pype', '..')))
)
progress += pype_inc
self._progress_callback(int(progress))
pype_root = include_dir.parent.resolve()
# generate list of filtered paths
dir_filter = [pype_root / f for f in self.pype_filter]
file: Path
for file in pype_list:
progress += pype_inc
self._progress_callback(int(progress))
# if file resides in filtered path, skip it
is_inside = None
df: Path
for df in dir_filter:
try:
is_inside = file.resolve().relative_to(df)
except ValueError:
pass
if is_inside:
continue
processed_path = file
self._log.debug(f"processing {processed_path}")
self._print(f"- processing {processed_path}", False)
zip_file.write(file,
"pype" / file.relative_to(pype_root))
# test if zip is ok
zip_file.testzip()
self._progress_callback(100)
@ -342,10 +463,15 @@ class BootstrapRepos:
This will enable Python to import modules is second-level directories
in zip file.
Adding to both `sys.path` and `PYTHONPATH`, skipping duplicates.
Args:
archive (str): path to archive.
archive (Path): path to archive.
"""
if not archive.is_file() and not archive.exists():
raise ValueError("Archive is not file.")
with ZipFile(archive, "r") as zip_file:
name_list = zip_file.namelist()
@ -362,8 +488,41 @@ class BootstrapRepos:
os.environ["PYTHONPATH"] = os.pathsep.join(paths)
@staticmethod
def add_paths_from_directory(directory: Path) -> None:
"""Add first level directories as paths to :mod:`sys.path`.
This works the same as :meth:`add_paths_from_archive` but in
specified directory.
Adding to both `sys.path` and `PYTHONPATH`, skipping duplicates.
Args:
directory (Path): path to directory.
"""
if not directory.exists() and not directory.is_dir():
raise ValueError("directory is invalid")
roots = []
for item in directory.iterdir():
if item.is_dir():
root = item.as_posix()
if root not in roots:
roots.append(root)
sys.path.insert(0, root)
pythonpath = os.getenv("PYTHONPATH", "")
paths = pythonpath.split(os.pathsep)
paths += roots
os.environ["PYTHONPATH"] = os.pathsep.join(paths)
def find_pype(
self, pype_path: Path = None) -> Union[List[PypeVersion], None]:
self,
pype_path: Path = None,
staging: bool = False,
include_zips: bool = False) -> Union[List[PypeVersion], None]:
"""Get ordered dict of detected Pype version.
Resolution order for Pype is following:
@ -374,6 +533,10 @@ class BootstrapRepos:
Args:
pype_path (Path, optional): Try to find Pype on the given path.
staging (bool, optional): Filter only staging version, skip them
otherwise.
include_zips (bool, optional): If set True it will try to find
Pype in zip files in given directory.
Returns:
dict of Path: Dictionary of detected Pype version.
@ -383,42 +546,110 @@ class BootstrapRepos:
"""
dir_to_search = self.data_dir
if os.getenv("PYPE_PATH"):
if Path(os.getenv("PYPE_PATH")).exists():
dir_to_search = Path(os.getenv("PYPE_PATH"))
else:
try:
registry_dir = Path(self.registry.get_item("pypePath"))
if registry_dir.exists():
dir_to_search = registry_dir
except ValueError:
# nothing found in registry, we'll use data dir
pass
# if we have pyp_path specified, search only there.
# if we have pype_path specified, search only there.
if pype_path:
dir_to_search = pype_path
else:
if os.getenv("PYPE_PATH"):
if Path(os.getenv("PYPE_PATH")).exists():
dir_to_search = Path(os.getenv("PYPE_PATH"))
else:
try:
registry_dir = Path(
str(self.registry.get_item("pypePath")))
if registry_dir.exists():
dir_to_search = registry_dir
except ValueError:
# nothing found in registry, we'll use data dir
pass
# pype installation dir doesn't exists
if not dir_to_search.exists():
return None
_pype_versions = []
file_pattern = re.compile(r"^pype-repositories-v(?P<version>\d+\.\d+\.\d*.+?).zip$") # noqa: E501
# iterate over directory in first level and find all that might
# contain Pype.
for file in dir_to_search.iterdir():
m = re.match(
file_pattern,
file.name)
if m:
try:
_pype_versions.append(
PypeVersion(
version=m.group("version"), path=file))
except ValueError:
# cannot parse version string
print(m)
pass
# if file, strip extension, in case of dir not.
name = file.name if file.is_dir() else file.stem
result = PypeVersion.version_in_str(name)
if result[0]:
detected_version: PypeVersion
detected_version = result[1]
if file.is_dir():
# if item is directory that might (based on it's name)
# contain Pype version, check if it really does contain
# Pype and that their versions matches.
try:
# add one 'pype' level as inside dir there should
# be many other repositories.
version_str = BootstrapRepos.get_version(
file / "pype")
version_check = PypeVersion(version=version_str)
except ValueError:
self._log.error(
f"cannot determine version from {file}")
continue
version_main = version_check.get_main_version()
detected_main = detected_version.get_main_version()
if version_main != detected_main:
self._log.error(
(f"dir version ({detected_version}) and "
f"its content version ({version_check}) "
"doesn't match. Skipping."))
continue
if file.is_file():
if not include_zips:
continue
# skip non-zip files
if file.suffix.lower() != ".zip":
continue
# open zip file, look inside and parse version from Pype
# inside it. If there is none, or it is different from
# version specified in file name, skip it.
try:
with ZipFile(file, "r") as zip_file:
with zip_file.open(
"pype/pype/version.py") as version_file:
zip_version = {}
exec(version_file.read(), zip_version)
version_check = PypeVersion(
version=zip_version["__version__"])
version_main = version_check.get_main_version() # noqa: E501
detected_main = detected_version.get_main_version() # noqa: E501
if version_main != detected_main:
self._log.error(
(f"zip version ({detected_version}) "
f"and its content version "
f"({version_check}) "
"doesn't match. Skipping."))
continue
except BadZipFile:
self._log.error(f"{file} is not zip file")
continue
except KeyError:
self._log.error("Zip not containing Pype")
continue
detected_version.path = file
if staging and detected_version.is_staging():
_pype_versions.append(detected_version)
if not staging and not detected_version.is_staging():
_pype_versions.append(detected_version)
return sorted(_pype_versions)
@ -426,16 +657,16 @@ class BootstrapRepos:
def _get_pype_from_mongo(mongo_url: str) -> Union[Path, None]:
"""Get path from Mongo database.
This sets environment variable ``AVALON_MONGO`` for
This sets environment variable ``PYPE_MONGO`` for
:mod:`pype.settings` to be able to read data from database.
It will then retrieve environment variables and among them
must be ``PYPE_ROOT``.
must be ``PYPE_PATH``.
Args:
mongo_url (str): mongodb connection url
Returns:
Path: if path from ``PYPE_ROOT`` is found.
Path: if path from ``PYPE_PATH`` is found.
None: if not.
"""
@ -479,11 +710,18 @@ class BootstrapRepos:
self._log.error(f"{pype_path} doesn't exists.")
return None
# find pype zip files in location. In that location, there can be
# either "live" Pype repository, or multiple zip files.
# test if entered path isn't user data dir
if self.data_dir == pype_path:
self._log.error("cannot point to user data dir")
return None
# find pype zip files in location. There can be
# either "live" Pype repository, or multiple zip files or even
# multiple pype version directories. This process looks into zip
# files and directories and tries to parse `version.py` file.
versions = self.find_pype(pype_path)
if versions:
self._log.info(f"found Pype zips in [ {pype_path} ].")
self._log.info(f"found Pype in [ {pype_path} ]")
self._log.info(f"latest version found is [ {versions[-1]} ]")
destination = self.data_dir / versions[-1].path.name
@ -503,13 +741,43 @@ class BootstrapRepos:
if not destination.parent.exists():
destination.parent.mkdir(parents=True)
# latest version found is directory
if versions[-1].path.is_dir():
# zip it, copy it and extract it
# create zip inside temporary directory.
self._log.info("Creating zip from directory ...")
with tempfile.TemporaryDirectory() as temp_dir:
temp_zip = \
Path(temp_dir) / f"pype-v{versions[-1]}.zip"
self._log.info(f"creating zip: {temp_zip}")
self._create_pype_zip(temp_zip, versions[-1].path)
if not os.path.exists(temp_zip):
self._log.error("make archive failed.")
return None
destination = self.data_dir / temp_zip.name
elif versions[-1].path.is_file():
# in this place, it must be zip file as `find_pype()` is
# checking just that.
assert versions[-1].path.suffix.lower() == ".zip", (
"Invalid file format"
)
try:
self._log.info("Copying zip to destination ...")
copyfile(versions[-1].path.as_posix(), destination.as_posix())
except OSError:
self._log.error(
"cannot copy detected version to user data directory",
exc_info=True)
return None
# extract zip there
self._log.info("extracting zip to destination ...")
with ZipFile(versions[-1].path, "r") as zip_ref:
zip_ref.extractall(destination)
return destination
# if we got here, it means that location is "live" Pype repository.
@ -518,4 +786,171 @@ class BootstrapRepos:
if not repo_file.exists():
self._log.error(f"installing zip {repo_file} failed.")
return None
return repo_file
destination = self.data_dir / repo_file.stem
if destination.exists():
try:
destination.unlink()
except OSError:
self._log.error(
f"cannot remove already existing {destination}",
exc_info=True)
return None
destination.mkdir(parents=True)
# extract zip there
self._log.info("extracting zip to destination ...")
with ZipFile(versions[-1].path, "r") as zip_ref:
zip_ref.extractall(destination)
return destination
def _print(self, message, error=False):
if self._message:
self._message.emit(message, error)
def extract_pype(self, version: PypeVersion) -> Union[Path, None]:
"""Extract zipped Pype version to user data directory.
Args:
version (PypeVersion): Version of Pype.
Returns:
Path: path to extracted version.
None: if something failed.
"""
if not version.path:
raise ValueError(
f"version {version} is not associated with any file")
destination = self.data_dir / version.path.stem
if destination.exists():
try:
destination.unlink()
except OSError as e:
msg = f"!!! Cannot remove already existing {destination}"
self._log.error(msg)
self._log.error(e.strerror)
self._print(msg, True)
self._print(e.strerror, True)
return None
destination.mkdir(parents=True)
# extract zip there
self._print("Extracting zip to destination ...")
with ZipFile(version.path, "r") as zip_ref:
zip_ref.extractall(destination)
self._print(f"Installed as {version.path.stem}")
return destination
def install_version(self, pype_version: PypeVersion, force: bool = False):
"""Install Pype version to user data directory.
Args:
pype_version (PypeVersion): Pype version to install.
force (bool, optional): Force overwrite existing version.
Returns:
Path: Path to installed Pype.
Raises:
PypeVersionExists: If not forced and this version already exist
in user data directory.
PypeVersionInvalid: If version to install is invalid.
PypeVersionIOError: If copying or zipping fail.
"""
# test if version is located (in user data dir)
is_inside = False
try:
is_inside = pype_version.path.resolve().relative_to(
self.data_dir)
except ValueError:
# if relative path cannot be calculated, Pype version is not
# inside user data dir
pass
if is_inside:
raise PypeVersionExists("Pype already inside user data dir")
# determine destination directory name
# for zip file strip suffix
destination = self.data_dir / pype_version.path.stem
# test if destination file already exist, if so lets delete it.
# we consider path on location as authoritative place.
if destination.exists() and force:
try:
destination.unlink()
except OSError:
self._log.error(
f"cannot remove already existing {destination}",
exc_info=True)
return None
else:
raise PypeVersionExists(f"{destination} already exist.")
# create destination parent directories even if they don't exist.
if not destination.exists():
destination.mkdir(parents=True)
# version is directory
if pype_version.path.is_dir():
# create zip inside temporary directory.
self._log.info("Creating zip from directory ...")
with tempfile.TemporaryDirectory() as temp_dir:
temp_zip = \
Path(temp_dir) / f"pype-v{pype_version}.zip"
self._log.info(f"creating zip: {temp_zip}")
self._create_pype_zip(temp_zip, pype_version.path)
if not os.path.exists(temp_zip):
self._log.error("make archive failed.")
raise PypeVersionIOError("Zip creation failed.")
# set zip as version source
pype_version.path = temp_zip
elif pype_version.path.is_file():
# check if file is zip (by extension)
if pype_version.path.suffix.lower() != ".zip":
raise PypeVersionInvalid("Invalid file format")
try:
# copy file to destination
self._log.info("Copying zip to destination ...")
copyfile(pype_version.path.as_posix(), destination.as_posix())
except OSError as e:
self._log.error(
"cannot copy version to user data directory",
exc_info=True)
raise PypeVersionIOError(
"can't copy version to destination") from e
# extract zip there
self._log.info("extracting zip to destination ...")
with ZipFile(pype_version.path, "r") as zip_ref:
zip_ref.extractall(destination)
return destination
class PypeVersionExists(Exception):
"""Exception for handling existing Pype version."""
pass
class PypeVersionInvalid(Exception):
"""Exception for handling invalid Pype version."""
pass
class PypeVersionIOError(Exception):
"""Exception for handling IO errors in Pype version."""
pass

View file

@ -19,7 +19,7 @@ class InstallDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(InstallDialog, self).__init__(parent)
self._mongo_url = ""
self._mongo_url = os.getenv("PYPE_MONGO", "")
self.setWindowTitle("Pype - Configure Pype repository path")
self._icon_path = os.path.join(
@ -149,10 +149,13 @@ class InstallDialog(QtWidgets.QDialog):
self.setLayout(mongo_layout)
def _mongo_changed(self, mongo: str):
self.parent()._mongo_url = mongo
self.parent().mongo_url = mongo
def get_mongo_url(self):
return self.parent()._mongo_url
return self.parent().mongo_url
def set_mongo_url(self, mongo: str):
self._mongo_input.setText(mongo)
def set_valid(self):
self._mongo_input.setStyleSheet(
@ -175,6 +178,8 @@ class InstallDialog(QtWidgets.QDialog):
)
self._mongo = MongoWidget(self)
if self._mongo_url:
self._mongo.set_mongo_url(self._mongo_url)
# Bottom button bar
# --------------------------------------------------------------------
@ -303,14 +308,17 @@ class InstallDialog(QtWidgets.QDialog):
options |= QtWidgets.QFileDialog.DontUseNativeDialog
options |= QtWidgets.QFileDialog.ShowDirsOnly
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
result = QtWidgets.QFileDialog.getExistingDirectory(
parent=self,
caption='Select path',
directory=os.getcwd(),
options=options)
if filename:
filename = QtCore.QDir.toNativeSeparators(filename)
if not result:
return
filename = result[0]
filename = QtCore.QDir.toNativeSeparators(filename)
if os.path.isdir(filename):
self.user_input.setText(filename)
@ -378,6 +386,7 @@ class InstallDialog(QtWidgets.QDialog):
if len(self._path) < 1:
self._mongo.setVisible(False)
return path
def _update_console(self, msg: str, error: bool = False) -> None:
"""Display message in console.

View file

@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
"""Working thread for installer."""
import os
import sys
from zipfile import ZipFile
from Qt.QtCore import QThread, Signal
from .bootstrap_repos import BootstrapRepos
from .bootstrap_repos import PypeVersion
from .tools import validate_mongo_connection
@ -43,8 +46,9 @@ class InstallThread(QThread):
self.message.emit("Installing Pype ...", False)
# find local version of Pype
bs = BootstrapRepos(progress_callback=self.set_progress)
local_version = bs.get_local_version()
bs = BootstrapRepos(
progress_callback=self.set_progress, message=self.message)
local_version = bs.get_local_live_version()
# if user did entered nothing, we install Pype from local version.
# zip content of `repos`, copy it to user data dir and append
@ -68,14 +72,71 @@ class InstallThread(QThread):
os.environ["PYPE_MONGO"] = self._mongo
self.message.emit(
f"Detecting installed Pype versions in {bs.data_dir}", False)
detected = bs.find_pype(include_zips=True)
if detected:
if PypeVersion(version=local_version) < detected[-1]:
self.message.emit((
f"Latest installed version {detected[-1]} is newer "
f"then currently running {local_version}"
), False)
self.message.emit("Skipping Pype install ...", False)
if detected[-1].path.suffix.lower() == ".zip":
bs.extract_pype(detected[-1])
return
if PypeVersion(version=local_version) == detected[-1]:
self.message.emit((
f"Latest installed version is the same as "
f"currently running {local_version}"
), False)
self.message.emit("Skipping Pype install ...", False)
return
self.message.emit((
"All installed versions are older then "
f"currently running one {local_version}"
), False)
else:
# we cannot build install package from frozen code.
if getattr(sys, 'frozen', False):
self.message.emit("None detected.", True)
self.message.emit(("Please set path to Pype sources to "
"build installation."), False)
return
else:
self.message.emit("None detected.", False)
self.message.emit(
f"We will use local Pype version {local_version}", False)
repo_file = bs.install_live_repos()
if not repo_file:
self.message.emit(
f"!!! install failed - {repo_file}", True)
f"!!! Install failed - {repo_file}", True)
return
self.message.emit(f"installed as {repo_file}", False)
destination = bs.data_dir / repo_file.stem
if destination.exists():
try:
destination.unlink()
except OSError as e:
self.message.emit(
f"!!! Cannot remove already existing {destination}",
True)
self.message.emit(e.strerror, True)
return
destination.mkdir(parents=True)
# extract zip there
self.message.emit("Extracting zip to destination ...", False)
with ZipFile(repo_file, "r") as zip_ref:
zip_ref.extractall(destination)
self.message.emit(f"Installed as {repo_file}", False)
else:
# if we have mongo connection string, validate it, set it to
# user settings and get PYPE_PATH from there.
@ -87,10 +148,14 @@ class InstallThread(QThread):
bs.registry.set_secure_item("pypeMongo", self._mongo)
os.environ["PYPE_MONGO"] = self._mongo
if os.getenv("PYPE_PATH") == self._path:
...
self.message.emit(f"processing {self._path}", True)
repo_file = bs.process_entered_location(self._path)
if not repo_file:
self.message.emit(f"!!! Cannot install", True)
self.message.emit("!!! Cannot install", True)
return
def set_path(self, path: str) -> None:

BIN
igniter/pype.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

413
igniter/splash.txt Normal file
View file

@ -0,0 +1,413 @@
*
.*
*
.*
*
.
*
.*
*
.
.
*
.*
.*
.*
*
.
.
*
.*
.*
.*
*
.
_.
/**
\ *
\*
*
*
.
__.
---*
\ \*
\ *
\*
*
.
\___.
/* *
\ \ *
\ \*
\ *
\*
.
|____.
/* *
\|\ *
\ \ *
\ \ *
\ \*
\/.
_/_____.
/* *
/ \ *
\ \ *
\ \ *
\ \__*
\/__.
__________.
--*-- ___*
\ \ \/_*
\ \ __*
\ \ \_*
\ \____\*
\/____/.
\____________ .
/* ___ \*
\ \ \/_\ *
\ \ _____*
\ \ \___/*
\ \____\ *
\/____/ .
|___________ .
/* ___ \ *
\|\ \/_\ \ *
\ \ _____/ *
\ \ \___/ *
\ \____\ / *
\/____/ \.
_/__________ .
/* ___ \ *
/ \ \/_\ \ *
\ \ _____/ *
\ \ \___/ ---*
\ \____\ / \__*
\/____/ \/__.
____________ .
--*-- ___ \ *
\ \ \/_\ \ *
\ \ _____/ *
\ \ \___/ ---- *
\ \____\ / \____\*
\/____/ \/____/.
____________
/\ ___ \ .
\ \ \/_\ \ *
\ \ _____/ *
\ \ \___/ ---- *
\ \____\ / \____\ .
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \ .
\ \ _____/ *
\ \ \___/ ---- *
\ \____\ / \____\ .
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ .
\ \ \___/ ---- *
\ \____\ / \____\ .
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/
\ \ \___/ ---- *
\ \____\ / \____\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/
\ \ \___/ ---- .
\ \____\ / \____\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ _
\ \ \___/ ----
\ \____\ / \____\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___
\ \ \___/ ----
\ \____\ / \____\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___
\ \ \___/ ---- \
\ \____\ / \____\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___
\ \ \___/ ---- \
\ \____\ / \____\ \
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___
\ \ \___/ ---- \
\ \____\ / \____\ __\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___
\ \ \___/ ---- \
\ \____\ / \____\ \__\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___
\ \ \___/ ---- \ \
\ \____\ / \____\ \__\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___
\ \ \___/ ---- \ \
\ \____\ / \____\ \__\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___.
\ \ \___/ ---- \ \\
\ \____\ / \____\ \__\,
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ .
\ \ \___/ ---- \ \\
\ \____\ / \____\ \__\\,
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ _.
\ \ \___/ ---- \ \\\
\ \____\ / \____\ \__\\\
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ __.
\ \ \___/ ---- \ \\ \
\ \____\ / \____\ \__\\_/.
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___.
\ \ \___/ ---- \ \\ \\
\ \____\ / \____\ \__\\__\.
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ .
\ \ \___/ ---- \ \\ \\
\ \____\ / \____\ \__\\__\\.
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ _.
\ \ \___/ ---- \ \\ \\\
\ \____\ / \____\ \__\\__\\.
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ __.
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\_.
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ __.
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__.
\/____/ \/____/
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ .
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ *
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ O*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ .oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ ..oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . .oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . p.oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . Py.oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYp.oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPe.oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE .oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE c.oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE C1.oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE ClU.oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE CluB.oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE Club .oO*
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE Club . ..
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE Club . ..
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE Club . .
____________
/\ ___ \
\ \ \/_\ \
\ \ _____/ ___ ___ ___
\ \ \___/ ---- \ \\ \\ \
\ \____\ / \____\ \__\\__\\__\
\/____/ \/____/ . PYPE Club .

View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""Pype terminal animation."""
import blessed
from pathlib import Path
from time import sleep
NO_TERMINAL = False
try:
term = blessed.Terminal()
except AttributeError:
# this happens when blessed cannot find proper terminal.
# If so, skip printing ascii art animation.
NO_TERMINAL = True
def play_animation():
"""Play ASCII art Pype animation."""
if NO_TERMINAL:
return
print(term.home + term.clear)
frame_size = 7
splash_file = Path(__file__).parent / "splash.txt"
with splash_file.open("r") as sf:
animation = sf.readlines()
animation_length = int(len(animation) / frame_size)
current_frame = 0
for _ in range(animation_length):
frame = "".join(
scanline
for y, scanline in enumerate(
animation[current_frame : current_frame + frame_size]
)
)
with term.location(0, 0):
# term.aquamarine3_bold(frame)
print(f"{term.bold}{term.aquamarine3}{frame}{term.normal}")
sleep(0.02)
current_frame += frame_size
print(term.move_y(7))

View file

@ -1,14 +1,104 @@
# -*- coding: utf-8 -*-
"""Tools used in **Igniter** GUI."""
"""Tools used in **Igniter** GUI.
Functions ``compose_url()`` and ``decompose_url()`` are the same as in
``pype.lib`` and they are here to avoid importing pype module before its
version is decided.
"""
import os
import sys
import uuid
from urllib.parse import urlparse
from typing import Dict
from urllib.parse import urlparse, parse_qs
from pymongo import MongoClient
from pymongo.errors import ServerSelectionTimeoutError, InvalidURI
from pype.lib import decompose_url, compose_url
def decompose_url(url: str) -> Dict:
"""Decompose mongodb url to its separate components.
Args:
url (str): Mongodb url.
Returns:
dict: Dictionary of components.
"""
components = {
"scheme": None,
"host": None,
"port": None,
"username": None,
"password": None,
"auth_db": None
}
result = urlparse(url)
if result.scheme is None:
_url = "mongodb://{}".format(url)
result = urlparse(_url)
components["scheme"] = result.scheme
components["host"] = result.hostname
try:
components["port"] = result.port
except ValueError:
raise RuntimeError("invalid port specified")
components["username"] = result.username
components["password"] = result.password
try:
components["auth_db"] = parse_qs(result.query)['authSource'][0]
except KeyError:
# no auth db provided, mongo will use the one we are connecting to
pass
return components
def compose_url(scheme: str = None,
host: str = None,
username: str = None,
password: str = None,
port: int = None,
auth_db: str = None) -> str:
"""Compose mongodb url from its individual components.
Args:
scheme (str, optional):
host (str, optional):
username (str, optional):
password (str, optional):
port (str, optional):
auth_db (str, optional):
Returns:
str: mongodb url
"""
url = "{scheme}://"
if username and password:
url += "{username}:{password}@"
url += "{host}"
if port:
url += ":{port}"
if auth_db:
url += "?authSource={auth_db}"
return url.format(**{
"scheme": scheme,
"host": host,
"username": username,
"password": password,
"port": port,
"auth_db": auth_db
})
def validate_mongo_connection(cnx: str) -> (bool, str):
@ -22,30 +112,29 @@ def validate_mongo_connection(cnx: str) -> (bool, str):
"""
parsed = urlparse(cnx)
if parsed.scheme in ["mongodb", "mongodb+srv"]:
# we have mongo connection string. Let's try if we can connect.
components = decompose_url(cnx)
mongo_args = {
"host": compose_url(**components),
"serverSelectionTimeoutMS": 1000
}
port = components.get("port")
if port is not None:
mongo_args["port"] = int(port)
try:
client = MongoClient(**mongo_args)
client.server_info()
except ServerSelectionTimeoutError as e:
return False, f"Cannot connect to server {cnx} - {e}"
except ValueError:
return False, f"Invalid port specified {parsed.port}"
except InvalidURI as e:
return False, str(e)
else:
return True, "Connection is successful"
else:
if parsed.scheme not in ["mongodb", "mongodb+srv"]:
return False, "Not mongodb schema"
# we have mongo connection string. Let's try if we can connect.
components = decompose_url(cnx)
mongo_args = {
"host": compose_url(**components),
"serverSelectionTimeoutMS": 1000
}
port = components.get("port")
if port is not None:
mongo_args["port"] = int(port)
try:
client = MongoClient(**mongo_args)
client.server_info()
except ServerSelectionTimeoutError as e:
return False, f"Cannot connect to server {cnx} - {e}"
except ValueError:
return False, f"Invalid port specified {parsed.port}"
except InvalidURI as e:
return False, str(e)
else:
return True, "Connection is successful"
def validate_path_string(path: str) -> (bool, str):
@ -81,26 +170,6 @@ def validate_path_string(path: str) -> (bool, str):
return False, "Not implemented yet"
def add_acre_to_sys_path():
"""Add full path of acre module to sys.path on ignitation."""
try:
# Skip if is possible to import
import acre
except ImportError:
# Full path to acred repository related to current file
acre_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"repos",
"acre"
)
# Add path to sys.path
sys.path.append(acre_dir)
# Validate that acre can be imported
import acre
def load_environments(sections: list = None) -> dict:
"""Load environments from Pype.
@ -114,7 +183,6 @@ def load_environments(sections: list = None) -> dict:
dict of str: loaded and processed environments.
"""
add_acre_to_sys_path()
import acre
from pype import settings
@ -131,5 +199,4 @@ def load_environments(sections: list = None) -> dict:
continue
merged_env = acre.append(merged_env, parsed_env)
env = acre.compute(merged_env, cleanup=True)
return env
return acre.compute(merged_env, cleanup=True)

466
igniter/user_settings.py Normal file
View file

@ -0,0 +1,466 @@
# -*- coding: utf-8 -*-
"""Package to deal with saving and retrieving user specific settings."""
import os
from datetime import datetime
from abc import ABCMeta, abstractmethod
import json
# disable lru cache in Python 2
try:
from functools import lru_cache
except ImportError:
def lru_cache(maxsize):
def max_size(func):
def wrapper(*args, **kwargs):
value = func(*args, **kwargs)
return value
return wrapper
return max_size
# ConfigParser was renamed in python3 to configparser
try:
import configparser
except ImportError:
import ConfigParser as configparser
import platform
import appdirs
import six
@six.add_metaclass(ABCMeta)
class ASettingRegistry():
"""Abstract class defining structure of **SettingRegistry** class.
It is implementing methods to store secure items into keyring, otherwise
mechanism for storing common items must be implemented in abstract
methods.
Attributes:
_name (str): Registry names.
"""
def __init__(self, name):
# type: (str) -> ASettingRegistry
super(ASettingRegistry, self).__init__()
if six.PY3:
import keyring
# hack for cx_freeze and Windows keyring backend
if platform.system() == "Windows":
from keyring.backends import Windows
keyring.set_keyring(Windows.WinVaultKeyring())
self._name = name
self._items = {}
def set_item(self, name, value):
# type: (str, str) -> None
"""Set item to settings registry.
Args:
name (str): Name of the item.
value (str): Value of the item.
"""
self._set_item(name, value)
@abstractmethod
def _set_item(self, name, value):
# type: (str, str) -> None
# Implement it
pass
def __setitem__(self, name, value):
self._items[name] = value
self._set_item(name, value)
def get_item(self, name):
# type: (str) -> str
"""Get item from settings registry.
Args:
name (str): Name of the item.
Returns:
value (str): Value of the item.
Raises:
ValueError: If item doesn't exist.
"""
return self._get_item(name)
@abstractmethod
def _get_item(self, name):
# type: (str) -> str
# Implement it
pass
def __getitem__(self, name):
return self._get_item(name)
def delete_item(self, name):
# type: (str) -> None
"""Delete item from settings registry.
Args:
name (str): Name of the item.
"""
self._delete_item(name)
@abstractmethod
def _delete_item(self, name):
# type: (str) -> None
"""Delete item from settings.
Note:
see :meth:`pype.lib.user_settings.ARegistrySettings.delete_item`
"""
pass
def __delitem__(self, name):
del self._items[name]
self._delete_item(name)
def set_secure_item(self, name, value):
# type: (str, str) -> None
"""Set sensitive item into system's keyring.
This uses `Keyring module`_ to save sensitive stuff into system's
keyring.
Args:
name (str): Name of the item.
value (str): Value of the item.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
if six.PY2:
raise NotImplementedError(
"Keyring not available on Python 2 hosts")
import keyring
keyring.set_password(self._name, name, value)
@lru_cache(maxsize=32)
def get_secure_item(self, name):
# type: (str) -> str
"""Get value of sensitive item from system's keyring.
See also `Keyring module`_
Args:
name (str): Name of the item.
Returns:
value (str): Value of the item.
Raises:
ValueError: If item doesn't exist.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
if six.PY2:
raise NotImplementedError(
"Keyring not available on Python 2 hosts")
import keyring
value = keyring.get_password(self._name, name)
if not value:
raise ValueError(
"Item {}:{} does not exist in keyring.".format(
self._name, name))
return value
def delete_secure_item(self, name):
# type: (str) -> None
"""Delete value stored in system's keyring.
See also `Keyring module`_
Args:
name (str): Name of the item to be deleted.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
if six.PY2:
raise NotImplementedError(
"Keyring not available on Python 2 hosts")
import keyring
self.get_secure_item.cache_clear()
keyring.delete_password(self._name, name)
class IniSettingRegistry(ASettingRegistry):
"""Class using :mod:`configparser`.
This class is using :mod:`configparser` (ini) files to store items.
"""
def __init__(self, name, path):
# type: (str, str) -> IniSettingRegistry
super(IniSettingRegistry, self).__init__(name)
# get registry file
version = os.getenv("PYPE_VERSION", "N/A")
self._registry_file = os.path.join(path, "{}.ini".format(name))
if not os.path.exists(self._registry_file):
with open(self._registry_file, mode="w") as cfg:
print("# Settings registry", cfg)
print("# Generated by Pype {}".format(version), cfg)
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
print("# {}".format(now), cfg)
def set_item_section(
self, section, name, value):
# type: (str, str, str) -> None
"""Set item to specific section of ini registry.
If section doesn't exists, it is created.
Args:
section (str): Name of section.
name (str): Name of the item.
value (str): Value of the item.
"""
value = str(value)
config = configparser.ConfigParser()
config.read(self._registry_file)
if not config.has_section(section):
config.add_section(section)
current = config[section]
current[name] = value
with open(self._registry_file, mode="w") as cfg:
config.write(cfg)
def _set_item(self, name, value):
# type: (str, str) -> None
self.set_item_section("MAIN", name, value)
def set_item(self, name, value):
# type: (str, str) -> None
"""Set item to settings ini file.
This saves item to ``DEFAULT`` section of ini as each item there
must reside in some section.
Args:
name (str): Name of the item.
value (str): Value of the item.
"""
# this does the some, overridden just for different docstring.
# we cast value to str as ini options values must be strings.
super(IniSettingRegistry, self).set_item(name, str(value))
def get_item(self, name):
# type: (str) -> str
"""Gets item from settings ini file.
This gets settings from ``DEFAULT`` section of ini file as each item
there must reside in some section.
Args:
name (str): Name of the item.
Returns:
str: Value of item.
Raises:
ValueError: If value doesn't exist.
"""
return super(IniSettingRegistry, self).get_item(name)
@lru_cache(maxsize=32)
def get_item_from_section(self, section, name):
# type: (str, str) -> str
"""Get item from section of ini file.
This will read ini file and try to get item value from specified
section. If that section or item doesn't exist, :exc:`ValueError`
is risen.
Args:
section (str): Name of ini section.
name (str): Name of the item.
Returns:
str: Item value.
Raises:
ValueError: If value doesn't exist.
"""
config = configparser.ConfigParser()
config.read(self._registry_file)
try:
value = config[section][name]
except KeyError:
raise ValueError(
"Registry doesn't contain value {}:{}".format(section, name))
return value
def _get_item(self, name):
# type: (str) -> str
return self.get_item_from_section("MAIN", name)
def delete_item_from_section(self, section, name):
# type: (str, str) -> None
"""Delete item from section in ini file.
Args:
section (str): Section name.
name (str): Name of the item.
Raises:
ValueError: If item doesn't exist.
"""
self.get_item_from_section.cache_clear()
config = configparser.ConfigParser()
config.read(self._registry_file)
try:
_ = config[section][name]
except KeyError:
raise ValueError(
"Registry doesn't contain value {}:{}".format(section, name))
config.remove_option(section, name)
# if section is empty, delete it
if len(config[section].keys()) == 0:
config.remove_section(section)
with open(self._registry_file, mode="w") as cfg:
config.write(cfg)
def _delete_item(self, name):
"""Delete item from default section.
Note:
See :meth:`~pype.lib.IniSettingsRegistry.delete_item_from_section`
"""
self.delete_item_from_section("MAIN", name)
class JSONSettingRegistry(ASettingRegistry):
"""Class using json file as storage."""
def __init__(self, name, path):
# type: (str, str) -> JSONSettingRegistry
super(JSONSettingRegistry, self).__init__(name)
#: str: name of registry file
self._registry_file = os.path.join(path, "{}.json".format(name))
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
header = {
"__metadata__": {
"pype-version": os.getenv("PYPE_VERSION", "N/A"),
"generated": now
},
"registry": {}
}
if not os.path.exists(os.path.dirname(self._registry_file)):
os.makedirs(os.path.dirname(self._registry_file), exist_ok=True)
if not os.path.exists(self._registry_file):
with open(self._registry_file, mode="w") as cfg:
json.dump(header, cfg, indent=4)
@lru_cache(maxsize=32)
def _get_item(self, name):
# type: (str) -> object
"""Get item value from registry json.
Note:
See :meth:`pype.lib.JSONSettingRegistry.get_item`
"""
with open(self._registry_file, mode="r") as cfg:
data = json.load(cfg)
try:
value = data["registry"][name]
except KeyError:
raise ValueError(
"Registry doesn't contain value {}".format(name))
return value
def get_item(self, name):
# type: (str) -> object
"""Get item value from registry json.
Args:
name (str): Name of the item.
Returns:
value of the item
Raises:
ValueError: If item is not found in registry file.
"""
return self._get_item(name)
def _set_item(self, name, value):
# type: (str, object) -> None
"""Set item value to registry json.
Note:
See :meth:`pype.lib.JSONSettingRegistry.set_item`
"""
with open(self._registry_file, "r+") as cfg:
data = json.load(cfg)
data["registry"][name] = value
cfg.truncate(0)
cfg.seek(0)
json.dump(data, cfg, indent=4)
def set_item(self, name, value):
# type: (str, object) -> None
"""Set item and its value into json registry file.
Args:
name (str): name of the item.
value (Any): value of the item.
"""
self._set_item(name, value)
def _delete_item(self, name):
# type: (str) -> None
self._get_item.cache_clear()
with open(self._registry_file, "r+") as cfg:
data = json.load(cfg)
del data["registry"][name]
cfg.truncate(0)
cfg.seek(0)
json.dump(data, cfg, indent=4)
class PypeSettingsRegistry(JSONSettingRegistry):
"""Class handling Pype general settings registry.
Attributes:
vendor (str): Name used for path construction.
product (str): Additional name used for path construction.
"""
def __init__(self):
self.vendor = "pypeclub"
self.product = "pype"
path = appdirs.user_data_dir(self.product, self.vendor)
super(PypeSettingsRegistry, self).__init__("pype_settings", path)

268
pype.py
View file

@ -1,268 +0,0 @@
# -*- coding: utf-8 -*-
"""Main entry point for Pype command.
Bootstrapping process of Pype is as follows:
`PYPE_PATH` is checked for existence - either one from environment or
from user settings. Precedence takes the one set by environment.
On this path we try to find zip files with `pype-repositories-v3.x.x.zip`
format.
If no Pype repositories are found in `PYPE_PATH (user data dir)
then **Igniter** (Pype setup tool) will launch its GUI.
It can be used to specify `PYPE_PATH` or if it is _not_ specified, current
*"live"* repositories will be used to create such zip file and copy it to
appdata dir in user home. Version will be determined by version specified
in Pype module.
If Pype repositories zip file is found in default install location
(user data dir) or in `PYPE_PATH`, it will get list of those zips there and
use latest one or the one specified with optional `--use-version` command
line argument. If the one specified doesn't exist then latest available
version will be used. All repositories in that zip will be added
to `sys.path` and `PYTHONPATH`.
If Pype is live (not frozen) then current version of Pype module will be
used. All directories under `repos` will be added to `sys.path` and
`PYTHONPATH`.
Pype depends on connection to `MongoDB`_. You can specify MongoDB connection
string via `AVALON_MONGO` set in environment or it can be set in user
settings or via **Igniter** GUI.
Todo:
Move or remove bootstrapping environments out of the code.
.. _MongoDB:
https://www.mongodb.com/
"""
import os
import re
import sys
import traceback
from igniter.tools import load_environments, add_acre_to_sys_path
from igniter import BootstrapRepos
try:
import acre
except ImportError:
add_acre_to_sys_path()
import acre
def set_environments() -> None:
"""Set loaded environments.
.. todo:
better handling of environments
"""
# FIXME: remove everything except global
env = load_environments(["global"])
env = acre.merge(env, dict(os.environ))
os.environ.clear()
os.environ.update(env)
def set_modules_environments():
"""Set global environments for pype's modules.
This requires to have pype in `sys.path`.
"""
from pype.modules import ModulesManager
modules_manager = ModulesManager()
module_envs = modules_manager.collect_global_environments()
publish_plugin_dirs = modules_manager.collect_plugin_paths()["publish"]
# Set pyblish plugins paths if any module want to register them
if publish_plugin_dirs:
publish_paths_str = os.environ.get("PYBLISHPLUGINPATH") or ""
publish_paths = publish_paths_str.split(os.pathsep)
_publish_paths = set()
for path in publish_paths:
if path:
_publish_paths.add(os.path.normpath(path))
for path in publish_plugin_dirs:
_publish_paths.add(os.path.normpath(path))
module_envs["PYBLISHPLUGINPATH"] = os.pathsep.join(_publish_paths)
# Metge environments with current environments and update values
if module_envs:
parsed_envs = acre.parse(module_envs)
env = acre.merge(parsed_envs, dict(os.environ))
os.environ.clear()
os.environ.update(env)
def boot():
"""Bootstrap Pype."""
from pype.lib.terminal_splash import play_animation
play_animation()
# find pype versions
bootstrap = BootstrapRepos()
pype_versions = bootstrap.find_pype()
# check for `--use-version=3.0.0` argument.
use_version = None
for arg in sys.argv:
m = re.search(r"--use-version=(?P<version>\d+\.\d+\.\d*.+?)", arg)
if m and m.group('version'):
use_version = m.group('version')
break
if not os.getenv("PYPE_MONGO"):
try:
pype_mongo = bootstrap.registry.get_secure_item("pypeMongo")
except ValueError:
print("*** No DB connection string specified.")
print("--- launching setup UI ...")
import igniter
igniter.run()
return
else:
os.environ["PYPE_MONGO"] = pype_mongo
set_environments()
if getattr(sys, 'frozen', False):
if not pype_versions:
import igniter
igniter.run()
version_path = BootstrapRepos.get_version_path_from_list(
use_version, pype_versions)
if version_path:
# use specified
bootstrap.add_paths_from_archive(version_path)
else:
if use_version is not None:
print(("!!! Specified version was not found, using "
"latest available"))
# use latest
version_path = pype_versions[-1].path
bootstrap.add_paths_from_archive(version_path)
use_version = str(pype_versions[-1])
os.environ["PYPE_ROOT"] = version_path.as_posix()
else:
# run through repos and add them to sys.path and PYTHONPATH
pype_root = os.path.dirname(os.path.realpath(__file__))
local_version = bootstrap.get_local_version()
if use_version and use_version != local_version:
version_path = BootstrapRepos.get_version_path_from_list(
use_version, pype_versions)
if version_path:
# use specified
bootstrap.add_paths_from_archive(version_path)
os.environ["PYPE_ROOT"] = pype_root
repos = os.listdir(os.path.join(pype_root, "repos"))
repos = [os.path.join(pype_root, "repos", repo) for repo in repos]
# add self to python paths
repos.insert(0, pype_root)
for repo in repos:
sys.path.append(repo)
pythonpath = os.getenv("PYTHONPATH", "")
paths = pythonpath.split(os.pathsep)
paths += repos
os.environ["PYTHONPATH"] = os.pathsep.join(paths)
# DEPRECATED: remove when `pype-config` dissolves into Pype for good.
# .-=-----------------------=-=. ^ .=-=--------------------------=-.
os.environ["PYPE_MODULE_ROOT"] = os.environ["PYPE_ROOT"]
# delete Pype module from cache so it is used from specific version
try:
del sys.modules["pype"]
del sys.modules["pype.version"]
except AttributeError:
pass
from pype import cli
from pype.lib import terminal as t
from pype.version import __version__
print(">>> loading environments ...")
set_environments()
set_modules_environments()
info = get_info()
info.insert(0, ">>> Using Pype from [ {} ]".format(
os.path.dirname(cli.__file__)))
info_length = len(max(info, key=len))
info.insert(0, f"*** Pype [{__version__}] " + "-" * info_length)
for i in info:
t.echo(i)
try:
cli.main(obj={}, prog_name="pype")
except Exception:
exc_info = sys.exc_info()
print("!!! Pype crashed:")
traceback.print_exception(*exc_info)
sys.exit(1)
def get_info() -> list:
"""Print additional information to console."""
from pype.lib.mongo import get_default_components
from pype.lib.log import PypeLogger
components = get_default_components()
infos = []
if not getattr(sys, 'frozen', False):
infos.append(("Pype variant", "staging"))
else:
infos.append(("Pype variant", "production"))
infos.append(("Running pype from", os.environ.get('PYPE_ROOT')))
infos.append(("Using mongodb", components["host"]))
if os.environ.get("FTRACK_SERVER"):
infos.append(("Using FTrack at",
os.environ.get("FTRACK_SERVER")))
if os.environ.get('DEADLINE_REST_URL'):
infos.append(("Using Deadline webservice at",
os.environ.get("DEADLINE_REST_URL")))
if os.environ.get('MUSTER_REST_URL'):
infos.append(("Using Muster at",
os.environ.get("MUSTER_REST_URL")))
# Reinitialize
PypeLogger.initialize()
log_components = PypeLogger.log_mongo_url_components
if log_components["host"]:
infos.append(("Logging to MongoDB", log_components["host"]))
infos.append((" - port", log_components["port"] or "<N/A>"))
infos.append((" - database", PypeLogger.log_database_name))
infos.append((" - collection", PypeLogger.log_collection_name))
infos.append((" - user", log_components["username"] or "<N/A>"))
if log_components["auth_db"]:
infos.append((" - auth source", log_components["auth_db"]))
maximum = max([len(i[0]) for i in infos])
formatted = []
for info in infos:
padding = (maximum - len(info[0])) + 1
formatted.append(
"... {}:{}[ {} ]".format(info[0], " " * padding, info[1]))
return formatted
if __name__ == "__main__":
boot()

View file

@ -1,13 +1,20 @@
# -*- coding: utf-8 -*-
"""Package for handling pype command line arguments."""
import os
import sys
import click
# import sys
from .pype_commands import PypeCommands
import click
@click.group(invoke_without_command=True)
@click.pass_context
@click.option("--use-version",
expose_value=False, help="use specified version")
@click.option("--use-staging", is_flag=True,
expose_value=False, help="use staging variants")
def main(ctx):
"""Pype is main command serving as entry point to pipeline system.
@ -20,6 +27,7 @@ def main(ctx):
@main.command()
@click.option("-d", "--dev", is_flag=True, help="Settings in Dev mode")
def settings(dev=False):
"""Show Pype Settings UI."""
PypeCommands().launch_settings_gui(dev)
@ -38,12 +46,6 @@ def tray(debug=False):
PypeCommands().launch_tray(debug)
@main.command()
def mongodb():
"""Launch local mongodb server. Useful for development."""
PypeCommands().launch_local_mongodb()
@main.command()
@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
@click.option("--ftrack-url", envvar="FTRACK_SERVER",
@ -56,7 +58,7 @@ def mongodb():
envvar="FTRACK_EVENTS_PATH",
help=("path to ftrack event handlers"))
@click.option("--no-stored-credentials", is_flag=True,
help="dont use stored credentials")
help="don't use stored credentials")
@click.option("--store-credentials", is_flag=True,
help="store provided credentials")
@click.option("--legacy", is_flag=True,
@ -165,41 +167,6 @@ def texturecopy(debug, project, asset, path):
PypeCommands().texture_copy(project, asset, path)
@main.command()
@click.option("-k", "--keyword", help="select tests by keyword to run",
type=click.STRING)
@click.argument("id", nargs=-1, type=click.STRING)
def test(pype, keyword, id):
"""Run test suite."""
if pype:
PypeCommands().run_pype_tests(keyword, id)
@main.command()
def make_docs():
"""Generate documentation with Sphinx into `docs/build`."""
PypeCommands().make_docs()
@main.command()
def coverage():
"""Generate code coverage report."""
PypeCommands().pype_setup_coverage()
@main.command()
def clean():
"""Delete python bytecode files.
Working throughout Pype directory, it will remove all pyc bytecode files.
This is normally not needed but there are cases when update of repostories
caused errors thanks to these files. If you encounter errors complaining
about `magic number`, run this command.
"""
# TODO: reimplement in Python
pass
@main.command(context_settings={"ignore_unknown_options": True})
@click.option("--app", help="Registered application name")
@click.option("--project", help="Project name",
@ -233,7 +200,9 @@ def launch(app, project, asset, task,
Optionally you can specify ftrack credentials if needed.
ARGUMENTS are passed to launched application.
"""
# TODO: this needs to switch for Settings
if ftrack_server:
os.environ["FTRACK_SERVER"] = ftrack_server
@ -255,6 +224,33 @@ def launch(app, project, asset, task,
@main.command()
def validate_config():
"""Validate all json configuration files for errors."""
PypeCommands().validate_jsons()
@click.option("-p", "--path", help="Path to zip file", default=None)
def generate_zip(path):
"""Generate Pype zip from current sources.
If PATH is not provided, it will create zip file in user data dir.
"""
PypeCommands().generate_zip(path)
@main.command(
context_settings=dict(
ignore_unknown_options=True,
allow_extra_args=True))
@click.argument("script", required=True, type=click.Path(exists=True))
def run(script):
"""Run python script in Pype context."""
import runpy
if not script:
print("Error: missing path to script file.")
else:
args = sys.argv
args.remove("run")
args.remove(script)
sys.argv = args
args_string = " ".join(args[1:])
print(f"... running: {script} {args_string}")
runpy.run_path(script, run_name="__main__", )

View file

@ -1,5 +1,16 @@
# -*- coding: utf-8 -*-
# flake8: noqa E402
"""Pype module API."""
# add vendor to sys path based on Python version
import sys
import os
import site
# add Python version specific vendor folder
site.addsitedir(
os.path.join(
os.getenv("PYPE_ROOT", ""),
"vendor", "python", "python_{}".format(sys.version[0])))
from .terminal import Terminal
from .execute import (

View file

@ -4,12 +4,20 @@ import blessed
from pathlib import Path
from time import sleep
NO_TERMINAL = False
term = blessed.Terminal()
try:
term = blessed.Terminal()
except AttributeError:
# this happens when blessed cannot find proper terminal.
# If so, skip printing ascii art animation.
NO_TERMINAL = True
def play_animation():
"""Play ASCII art Pype animation."""
if NO_TERMINAL:
return
print(term.home + term.clear)
frame_size = 7
splash_file = Path(__file__).parent / "splash.txt"
@ -19,11 +27,12 @@ def play_animation():
animation_length = int(len(animation) / frame_size)
current_frame = 0
for _ in range(animation_length):
frame = ""
y = 0
for scanline in animation[current_frame:current_frame + frame_size]:
frame += scanline
y += 1
frame = "".join(
scanline
for y, scanline in enumerate(
animation[current_frame: current_frame + frame_size]
)
)
with term.location(0, 0):
# term.aquamarine3_bold(frame)

View file

@ -28,8 +28,6 @@ import platform
import appdirs
import six
from ..version import __version__
@six.add_metaclass(ABCMeta)
class ASettingRegistry():
@ -213,11 +211,12 @@ class IniSettingRegistry(ASettingRegistry):
# type: (str, str) -> IniSettingRegistry
super(IniSettingRegistry, self).__init__(name)
# get registry file
version = os.getenv("PYPE_VERSION", "N/A")
self._registry_file = os.path.join(path, "{}.ini".format(name))
if not os.path.exists(self._registry_file):
with open(self._registry_file, mode="w") as cfg:
print("# Settings registry", cfg)
print("# Generated by Pype {}".format(__version__), cfg)
print("# Generated by Pype {}".format(version), cfg)
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
print("# {}".format(now), cfg)
@ -368,7 +367,7 @@ class JSONSettingRegistry(ASettingRegistry):
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
header = {
"__metadata__": {
"pype-version": __version__,
"pype-version": os.getenv("PYPE_VERSION", "N/A"),
"generated": now
},
"registry": {}
@ -459,9 +458,9 @@ class PypeSettingsRegistry(JSONSettingRegistry):
product (str): Additional name used for path construction.
"""
vendor = "pypeclub"
product = "pype"
def __init__(self):
self.vendor = "pypeclub"
self.product = "pype"
path = appdirs.user_data_dir(self.product, self.vendor)
super(PypeSettingsRegistry, self).__init__("pype_settings", path)

View file

@ -57,9 +57,15 @@ class SocketThread(threading.Thread):
env = os.environ.copy()
env["PYPE_PROCESS_MONGO_ID"] = str(Logger.mongo_process_id)
executable_args = [
sys.executable
]
if getattr(sys, "frozen", False):
executable_args.append("run")
self.subproc = subprocess.Popen(
[
sys.executable,
*executable_args,
self.filepath,
*self.additional_args,
str(self.port)

View file

@ -4,7 +4,7 @@ import signal
import socket
import datetime
from ftrack_server import FtrackServer
from pype.modules.ftrack.ftrack_server.ftrack_server import FtrackServer
from pype.modules.ftrack.ftrack_server.lib import (
SocketSession,
ProcessEventHub,

View file

@ -7,7 +7,7 @@ import socket
import datetime
import ftrack_api
from ftrack_server import FtrackServer
from pype.modules.ftrack.ftrack_server.ftrack_server import FtrackServer
from pype.modules.ftrack.ftrack_server.lib import (
SocketSession,
StatusEventHub,

View file

@ -6,7 +6,7 @@ import socket
import pymongo
import ftrack_api
from ftrack_server import FtrackServer
from pype.modules.ftrack.ftrack_server.ftrack_server import FtrackServer
from pype.modules.ftrack.ftrack_server.lib import (
SocketSession,
StorerEventHub,

View file

@ -4,10 +4,10 @@ import datetime
import signal
import threading
from ftrack_server import FtrackServer
import ftrack_api
from pype.api import Logger
from pype.modules import ModulesManager
from pype.modules.ftrack.ftrack_server.ftrack_server import FtrackServer
log = Logger().get_logger("Event Server Legacy")

View file

@ -2,7 +2,7 @@ import sys
import signal
import socket
from ftrack_server import FtrackServer
from pype.modules.ftrack.ftrack_server.ftrack_server import FtrackServer
from pype.modules.ftrack.ftrack_server.lib import (
SocketSession,
SocketBaseEventHub

View file

@ -8,6 +8,7 @@ from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
from pype.api import Logger
from pype.api import get_system_settings
from ..utils import time_function
import time
SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly',
'https://www.googleapis.com/auth/drive.file',
@ -42,6 +43,7 @@ class GDriveHandler(AbstractProvider):
"""
FOLDER_STR = 'application/vnd.google-apps.folder'
MY_DRIVE_STR = 'My Drive' # name of root folder of regular Google drive
CHUNK_SIZE = 2097152 # must be divisible by 256!
def __init__(self, site_name, tree=None, presets=None):
self.presets = None
@ -277,7 +279,9 @@ class GDriveHandler(AbstractProvider):
path = new_path_key
return folder_id
def upload_file(self, source_path, path, overwrite=False):
def upload_file(self, source_path, path,
server, collection, file, representation, site,
overwrite=False):
"""
Uploads single file from 'source_path' to destination 'path'.
It creates all folders on the path if are not existing.
@ -287,6 +291,13 @@ class GDriveHandler(AbstractProvider):
path (string): absolute path with or without name of the file
overwrite (boolean): replace existing file
arguments for saving progress:
server (SyncServer): server instance to call update_db on
collection (str): name of collection
file (dict): info about uploaded file (matches structure from db)
representation (dict): complete repre containing 'file'
site (str): site name
Returns:
(string) file_id of created/modified file ,
throws FileExistsError, FileNotFoundError exceptions
@ -302,8 +313,8 @@ class GDriveHandler(AbstractProvider):
path = os.path.dirname(path)
else:
target_name = os.path.basename(source_path)
file = self.file_path_exists(path + "/" + target_name)
if file and not overwrite:
target_file = self.file_path_exists(path + "/" + target_name)
if target_file and not overwrite:
raise FileExistsError("File already exists, "
"use 'overwrite' argument")
@ -316,23 +327,45 @@ class GDriveHandler(AbstractProvider):
}
media = MediaFileUpload(source_path,
mimetype='application/octet-stream',
chunksize=self.CHUNK_SIZE,
resumable=True)
try:
if not file:
if not target_file:
# update doesnt like parent
file_metadata['parents'] = [folder_id]
file = self.service.files().create(body=file_metadata,
supportsAllDrives=True,
media_body=media,
fields='id').execute()
request = self.service.files().create(body=file_metadata,
supportsAllDrives=True,
media_body=media,
fields='id')
else:
file = self.service.files().update(fileId=file["id"],
body=file_metadata,
supportsAllDrives=True,
media_body=media,
fields='id').execute()
request = self.service.files().update(fileId=target_file["id"],
body=file_metadata,
supportsAllDrives=True,
media_body=media,
fields='id')
media.stream()
log.debug("Start Upload! {}".format(source_path))
last_tick = status = response = None
status_val = 0
while response is None:
if status:
status_val = float(status.progress())
if not last_tick or \
time.time() - last_tick >= server.LOG_PROGRESS_SEC:
last_tick = time.time()
log.debug("Uploaded %d%%." %
int(status_val * 100))
server.update_db(collection=collection,
new_file_id=None,
file=file,
representation=representation,
site=site,
progress=status_val
)
status, response = request.next_chunk()
except errors.HttpError as ex:
if ex.resp['status'] == '404':
@ -344,13 +377,14 @@ class GDriveHandler(AbstractProvider):
log.warning("Forbidden received, hit quota. "
"Injecting 60s delay.")
import time
time.sleep(60)
return False
raise
return file["id"]
return response['id']
def download_file(self, source_path, local_path, overwrite=False):
def download_file(self, source_path, local_path,
server, collection, file, representation, site,
overwrite=False):
"""
Downloads single file from 'source_path' (remote) to 'local_path'.
It creates all folders on the local_path if are not existing.
@ -361,6 +395,13 @@ class GDriveHandler(AbstractProvider):
local_path (string): absolute path with or without name of the file
overwrite (boolean): replace existing file
arguments for saving progress:
server (SyncServer): server instance to call update_db on
collection (str): name of collection
file (dict): info about uploaded file (matches structure from db)
representation (dict): complete repre containing 'file'
site (str): site name
Returns:
(string) file_id of created/modified file ,
throws FileExistsError, FileNotFoundError exceptions
@ -378,9 +419,9 @@ class GDriveHandler(AbstractProvider):
else: # just folder, get file name from source
target_name = os.path.basename(source_path)
file = os.path.isfile(local_path + "/" + target_name)
local_file = os.path.isfile(local_path + "/" + target_name)
if file and not overwrite:
if local_file and not overwrite:
raise FileExistsError("File already exists, "
"use 'overwrite' argument")
@ -389,9 +430,24 @@ class GDriveHandler(AbstractProvider):
with open(local_path + "/" + target_name, "wb") as fh:
downloader = MediaIoBaseDownload(fh, request)
done = False
while done is False:
status, done = downloader.next_chunk()
last_tick = status = response = None
status_val = 0
while response is None:
if status:
status_val = float(status.progress())
if not last_tick or \
time.time() - last_tick >= server.LOG_PROGRESS_SEC:
last_tick = time.time()
log.debug("Downloaded %d%%." %
int(status_val * 100))
server.update_db(collection=collection,
new_file_id=None,
file=file,
representation=representation,
site=site,
progress=status_val
)
status, response = downloader.next_chunk()
return target_name

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

View file

@ -18,7 +18,7 @@ from .utils import time_function
import six
from pype.lib import PypeLogger
from .. import PypeModule, ITrayService
from .. import PypeModule, ITrayModule
if six.PY2:
web = asyncio = STATIC_DIR = WebSocketAsync = None
@ -34,7 +34,7 @@ class SyncStatus(Enum):
DO_DOWNLOAD = 2
class SyncServer(PypeModule, ITrayService):
class SyncServer(PypeModule, ITrayModule):
"""
Synchronization server that is syncing published files from local to
any of implemented providers (like GDrive, S3 etc.)
@ -92,6 +92,7 @@ class SyncServer(PypeModule, ITrayService):
# set 0 to no limit
REPRESENTATION_LIMIT = 100
DEFAULT_SITE = 'studio'
LOG_PROGRESS_SEC = 5 # how often log progress to DB
name = "sync_server"
label = "Sync Server"
@ -116,6 +117,8 @@ class SyncServer(PypeModule, ITrayService):
self.presets = None # settings for all enabled projects for sync
self.sync_server_thread = None # asyncio requires new thread
self.action_show_widget = None
def connect_with_modules(self, *_a, **kw):
return
@ -131,21 +134,26 @@ class SyncServer(PypeModule, ITrayService):
self.presets = None
self.lock = threading.Lock()
self.connection = AvalonMongoDB()
self.connection.install()
try:
self.presets = self.get_synced_presets()
self.set_active_sites(self.presets)
self.sync_server_thread = SyncServerThread(self)
from .tray.app import SyncServerWindow
self.widget = SyncServerWindow(self)
except ValueError:
log.info("No system setting for sync. Not syncing.")
log.info("No system setting for sync. Not syncing.", exc_info=True)
self.enabled = False
except KeyError:
log.info((
"There are not set presets for SyncServer OR "
"Credentials provided are invalid, "
"no syncing possible").
format(str(self.presets)), exc_info=True)
self.enabled = False
def tray_start(self):
"""
@ -185,6 +193,19 @@ class SyncServer(PypeModule, ITrayService):
exc_info=True
)
def tray_menu(self, parent_menu):
if not self.enabled:
return
from Qt import QtWidgets
"""Add menu or action to Tray(or parent)'s menu"""
action = QtWidgets.QAction("SyncServer", parent_menu)
action.triggered.connect(self.show_widget)
parent_menu.addAction(action)
parent_menu.addSeparator()
self.action_show_widget = action
@property
def is_running(self):
return self.sync_server_thread.is_running
@ -245,7 +266,8 @@ class SyncServer(PypeModule, ITrayService):
settings = get_project_settings(project_name)
sync_settings = settings.get("global")["sync_server"]
if not sync_settings:
log.info("No project setting for Sync Server, not syncing.")
log.info("No project setting for {}, not syncing.".
format(project_name))
return {}
if sync_settings.get("enabled"):
return sync_settings
@ -406,8 +428,8 @@ class SyncServer(PypeModule, ITrayService):
return SyncStatus.DO_NOTHING
async def upload(self, file, representation, provider_name, site_name,
tree=None, preset=None):
async def upload(self, collection, file, representation, provider_name,
site_name, tree=None, preset=None):
"""
Upload single 'file' of a 'representation' to 'provider'.
Source url is taken from 'file' portion, where {root} placeholder
@ -418,6 +440,7 @@ class SyncServer(PypeModule, ITrayService):
from GDrive), 'created_dt' - time of upload
Args:
collection (str): source collection
file (dictionary): of file from representation in Mongo
representation (dictionary): of representation
provider_name (string): gdrive, gdc etc.
@ -447,21 +470,28 @@ class SyncServer(PypeModule, ITrayService):
err = "Folder {} wasn't created. Check permissions.".\
format(target_folder)
raise NotADirectoryError(err)
_, remote_site = self.get_sites_for_project(collection)
loop = asyncio.get_running_loop()
file_id = await loop.run_in_executor(None,
handler.upload_file,
local_file,
remote_file,
True)
self,
collection,
file,
representation,
remote_site,
True
)
return file_id
async def download(self, file, representation, provider_name,
async def download(self, collection, file, representation, provider_name,
site_name, tree=None, preset=None):
"""
Downloads file to local folder denoted in representation.Context.
Args:
collection (str): source collection
file (dictionary) : info about processed file
representation (dictionary): repr that 'file' belongs to
provider_name (string): 'gdrive' etc
@ -485,26 +515,37 @@ class SyncServer(PypeModule, ITrayService):
local_folder = os.path.dirname(local_file)
os.makedirs(local_folder, exist_ok=True)
local_site, _ = self.get_sites_for_project(collection)
loop = asyncio.get_running_loop()
file_id = await loop.run_in_executor(None,
handler.download_file,
remote_file,
local_file,
False)
False,
self,
collection,
file,
representation,
local_site
)
return file_id
def update_db(self, new_file_id, file, representation, provider_name,
error=None):
def update_db(self, collection, new_file_id, file, representation,
site, error=None, progress=None):
"""
Update 'provider' portion of records in DB with success (file_id)
or error (exception)
Args:
collection (string): name of project - force to db connection as
each file might come from different collection
new_file_id (string):
file (dictionary): info about processed file (pulled from DB)
representation (dictionary): parent repr of file (from DB)
provider_name (string): label ('gdrive', 'S3')
site (string): label ('gdrive', 'S3')
error (string): exception message
progress (float): 0-1 of progress of upload/download
Returns:
None
@ -518,26 +559,33 @@ class SyncServer(PypeModule, ITrayService):
file_index, _ = self._get_file_info(representation.get('files', []),
file_id)
site_index, _ = self._get_provider_rec(file.get('sites', []),
provider_name)
site)
update = {}
if new_file_id:
update["$set"] = self._get_success_dict(file_index, site_index,
new_file_id)
# reset previous errors if any
update["$unset"] = self._get_error_dict(file_index, site_index,
"", "")
"", "", "")
elif progress is not None:
update["$set"] = self._get_progress_dict(file_index, site_index,
progress)
else:
tries = self._get_tries_count(file, provider_name)
tries = self._get_tries_count(file, site)
tries += 1
update["$set"] = self._get_error_dict(file_index, site_index,
error, tries)
self.connection.Session["AVALON_PROJECT"] = collection
self.connection.update_one(
query,
update
)
if progress is not None:
return
status = 'failed'
error_str = 'with error {}'.format(error)
if new_file_id:
@ -553,7 +601,7 @@ class SyncServer(PypeModule, ITrayService):
def _get_file_info(self, files, _id):
"""
Return record from list of records which name matches to 'provider'
Could be possibly refactored with '_get_file_info' together.
Could be possibly refactored with '_get_provider_rec' together.
Args:
files (list): of dictionaries with info about published files
@ -590,7 +638,7 @@ class SyncServer(PypeModule, ITrayService):
return -1, None
def reset_provider_for_file(self, collection, representation_id,
file_id, site_name):
file_id, side):
"""
Reset information about synchronization for particular 'file_id'
and provider.
@ -599,7 +647,7 @@ class SyncServer(PypeModule, ITrayService):
collection (string): name of project (eg. collection) in DB
representation_id(string): _id of representation
file_id (string): file _id in representation
site_name (string): 'gdrive', 'S3' etc
side (string): local or remote side
Returns:
None
"""
@ -607,19 +655,25 @@ class SyncServer(PypeModule, ITrayService):
query = {
"_id": ObjectId(representation_id)
}
self.connection.Session["AVALON_PROJECT"] = collection
representation = list(self.connection.find(query))
representation = list(self.connection.database[collection].find(query))
if not representation:
raise ValueError("Representation {} not found in {}".
format(representation_id, collection))
local_site, remote_site = self.get_sites_for_project(collection)
if side == 'local':
site_name = local_site
else:
site_name = remote_site
files = representation[0].get('files', [])
file_index, _ = self._get_file_info(files,
file_id)
site_index, _ = self._get_provider_rec(files[file_index].
get('sites', []),
site_name)
if file_index > 0 and site_index > 0:
if file_index >= 0 and site_index >= 0:
elem = {"name": site_name}
update = {
"$set": {"files.{}.sites.{}".format(file_index, site_index):
@ -627,7 +681,7 @@ class SyncServer(PypeModule, ITrayService):
}
}
self.connection.update_one(
self.connection.database[collection].update_one(
query,
update
)
@ -641,6 +695,10 @@ class SyncServer(PypeModule, ITrayService):
"""
return int(self.presets[project_name]["config"]["loop_delay"])
def show_widget(self):
"""Show dialog to enter credentials"""
self.widget.show()
def _get_success_dict(self, file_index, site_index, new_file_id):
"""
Provide success metadata ("id", "created_dt") to be stored in Db.
@ -660,7 +718,8 @@ class SyncServer(PypeModule, ITrayService):
datetime.utcnow()}
return val
def _get_error_dict(self, file_index, site_index, error="", tries=""):
def _get_error_dict(self, file_index, site_index,
error="", tries="", progress=""):
"""
Provide error metadata to be stored in Db.
Used for set (error and tries provided) or unset mode.
@ -675,7 +734,9 @@ class SyncServer(PypeModule, ITrayService):
val = {"files.{}.sites.{}.last_failed_dt".
format(file_index, site_index): datetime.utcnow(),
"files.{}.sites.{}.error".format(file_index, site_index): error,
"files.{}.sites.{}.tries".format(file_index, site_index): tries
"files.{}.sites.{}.tries".format(file_index, site_index): tries,
"files.{}.sites.{}.progress".format(file_index, site_index):
progress
}
return val
@ -703,6 +764,22 @@ class SyncServer(PypeModule, ITrayService):
_, rec = self._get_provider_rec(file.get("sites", []), provider)
return rec.get("tries", 0)
def _get_progress_dict(self, file_index, site_index, progress):
"""
Provide progress metadata to be stored in Db.
Used during upload/download for GUI to show.
Args:
file_index: (int) - index of modified file
site_index: (int) - index of modified site of modified file
progress: (float) - 0-1 progress of upload/download
Returns:
(dictionary)
"""
val = {"files.{}.sites.{}.progress".
format(file_index, site_index): progress
}
return val
def _get_local_file_path(self, file, local_root):
"""
Auxiliary function for replacing rootless path with real path
@ -848,23 +925,27 @@ class SyncServerThread(threading.Thread):
tree = handler.get_tree()
limit -= 1
task = asyncio.create_task(
self.module.upload(file,
self.module.upload(collection,
file,
sync,
provider,
site,
tree,
site_preset))
task_files_to_process.append(task)
# store info for exception handling
# store info for exception handlingy
files_processed_info.append((file,
sync,
site))
site,
collection
))
processed_file_path.add(file_path)
if status == SyncStatus.DO_DOWNLOAD:
tree = handler.get_tree()
limit -= 1
task = asyncio.create_task(
self.module.download(file,
self.module.download(collection,
file,
sync,
provider,
site,
@ -874,7 +955,9 @@ class SyncServerThread(threading.Thread):
files_processed_info.append((file,
sync,
local))
local,
collection
))
processed_file_path.add(file_path)
log.debug("Sync tasks count {}".
@ -884,12 +967,13 @@ class SyncServerThread(threading.Thread):
return_exceptions=True)
for file_id, info in zip(files_created,
files_processed_info):
file, representation, site = info
file, representation, site, collection = info
error = None
if isinstance(file_id, BaseException):
error = str(file_id)
file_id = None
self.module.update_db(file_id,
self.module.update_db(collection,
file_id,
file,
representation,
site,

File diff suppressed because it is too large Load diff

View file

@ -814,9 +814,9 @@ class ExtractBurnin(pype.api.Extractor):
"""Return path to python script for burnin processing."""
# TODO maybe convert to Plugin's attribute
# Get script path.
module_path = os.environ["PYPE_MODULE_ROOT"]
module_path = os.environ["PYPE_ROOT"]
# There can be multiple paths in PYPE_MODULE_ROOT, in which case
# There can be multiple paths in PYPE_ROOT, in which case
# we just take first one.
if os.pathsep in module_path:
module_path = module_path.split(os.pathsep)[0]

View file

@ -908,7 +908,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
file_info = self.prepare_file_info(path,
integrated_file_sizes[dest],
file_hash)
file_hash,
instance=instance)
output_resources.append(file_info)
return output_resources
@ -928,7 +929,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
dest += '.{}'.format(self.TMP_FILE_EXT)
return dest
def prepare_file_info(self, path, size=None, file_hash=None, sites=None):
def prepare_file_info(self, path, size=None, file_hash=None,
sites=None, instance=None):
""" Prepare information for one file (asset or resource)
Arguments:
@ -938,6 +940,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
sites(optional): array of published locations,
[ {'name':'studio', 'created_dt':date} by default
keys expected ['studio', 'site1', 'gdrive1']
instance(dict, optional): to get collected settings
Returns:
rec: dictionary with filled info
"""
@ -945,15 +948,18 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
remote_site = None
sync_server_presets = None
# manager = ModulesManager()
# sync_server = manager.modules_by_name["sync_server"]
# try:
# if sync_server.enabled:
# local_site, remote_site = sync_server.get_sites_for_project()
# except ValueError:
# log.debug(("There are not set presets for SyncServer."
# " No credentials provided, no synching possible").
# format(str(sync_server_presets)))
if (instance.context.data["system_settings"]
["modules"]
["sync_server"]
["enabled"]):
sync_server_presets = (instance.context.data["project_settings"]
["global"]
["sync_server"])
if sync_server_presets["enabled"]:
local_site = sync_server_presets["config"].\
get("active_site", "studio").strip()
remote_site = sync_server_presets["config"].get("remote_site")
rec = {
"_id": io.ObjectId(),

View file

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""Implementation of Pype commands."""
import os
import subprocess
import sys
from pathlib import Path
from pype.lib import PypeLogger
@ -61,3 +62,27 @@ class PypeCommands:
def validate_jsons(self):
pass
@staticmethod
def generate_zip(out_path: str):
"""Generate zip file from current sources.
Args:
out_path (str): Path to generated zip file.
"""
from igniter import bootstrap_repos
# create zip file
bs = bootstrap_repos.BootstrapRepos()
if out_path:
out_path = Path(out_path)
bs.data_dir = out_path.parent
print(f">>> Creating zip in {bs.data_dir} ...")
repo_file = bs.install_live_repos()
if not repo_file:
print("!!! Error while creating zip file.")
exit(1)
print(f">>> Created {repo_file}")

View file

@ -13,14 +13,13 @@
"MAYA_DISABLE_CER",
"PYMEL_SKIP_MEL_INIT",
"LC_ALL",
"PYPE_LOG_NO_COLORS",
"MAYA_TEST"
"PYPE_LOG_NO_COLORS"
]
},
"PYTHONPATH": [
"{PYPE_MODULE_ROOT}/repos/avalon-core/setup/maya",
"{PYPE_MODULE_ROOT}/repos/maya-look-assigner",
"{PYTHON_ENV}/python2/Lib/site-packages",
"{PYPE_ROOT}/pype/setup/maya",
"{PYPE_REPOS_ROOT}/avalon-core/setup/maya",
"{PYPE_REPOS_ROOT}/maya-look-assigner",
"{PYTHONPATH}"
],
"MAYA_DISABLE_CLIC_IPM": "Yes",
@ -28,8 +27,7 @@
"MAYA_DISABLE_CER": "Yes",
"PYMEL_SKIP_MEL_INIT": "Yes",
"LC_ALL": "C",
"PYPE_LOG_NO_COLORS": "Yes",
"MAYA_TEST": "{MAYA_VERSION}"
"PYPE_LOG_NO_COLORS": "Yes"
},
"variants": {
"maya_2020": {
@ -140,8 +138,8 @@
]
},
"PYTHONPATH": [
"{PYPE_MODULE_ROOT}/repos/avalon-core/setup/maya",
"{PYPE_MODULE_ROOT}/repos/maya-look-assigner",
"{PYPE_REPOS_ROOT}/avalon-core/setup/maya",
"{PYPE_REPOS_ROOT}/maya-look-assigner",
"{PYTHON_ENV}/python2/Lib/site-packages",
"{PYTHONPATH}"
],
@ -242,7 +240,7 @@
]
},
"NUKE_PATH": [
"{PYPE_ROOT}/repos/avalon-core/setup/nuke/nuke_path",
"{PYPE_REPOS_ROOT}/avalon-core/setup/nuke/nuke_path",
"{PYPE_ROOT}/pype/hosts/nuke/startup",
"{PYPE_STUDIO_PLUGINS}/nuke"
],
@ -367,7 +365,7 @@
]
},
"NUKE_PATH": [
"{PYPE_ROOT}/repos/avalon-core/setup/nuke/nuke_path",
"{PYPE_REPOS_ROOT}/avalon-core/setup/nuke/nuke_path",
"{PYPE_ROOT}/pype/hosts/nuke/startup",
"{PYPE_STUDIO_PLUGINS}/nuke"
],
@ -494,7 +492,7 @@
]
},
"HIERO_PLUGIN_PATH": [
"{PYPE_MODULE_ROOT}/setup/hiero/hiero_plugin_path"
"{PYPE_ROOT}/setup/hiero/hiero_plugin_path"
],
"PATH": {
"windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}"
@ -616,7 +614,7 @@
]
},
"HIERO_PLUGIN_PATH": [
"{PYPE_MODULE_ROOT}/setup/hiero/hiero_plugin_path"
"{PYPE_ROOT}/setup/hiero/hiero_plugin_path"
],
"PATH": {
"windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}"
@ -815,8 +813,6 @@
},
"PYTHONPATH": [
"{PYTHON36_RESOLVE}/Lib/site-packages",
"{VIRTUAL_ENV}/Lib/site-packages",
"{PYTHONPATH}",
"{RESOLVE_SCRIPT_API}/Modules",
"{PYTHONPATH}"
],
@ -825,7 +821,7 @@
"{PYTHON36_RESOLVE}/Scripts",
"{PATH}"
],
"PRE_PYTHON_SCRIPT": "{PYPE_MODULE_ROOT}/pype/resolve/preload_console.py",
"PRE_PYTHON_SCRIPT": "{PYPE_ROOT}/pype/resolve/preload_console.py",
"PYPE_LOG_NO_COLORS": "True",
"RESOLVE_DEV": "True"
},
@ -866,14 +862,14 @@
]
},
"HOUDINI_PATH": {
"darwin": "{PYPE_MODULE_ROOT}/setup/houdini:&",
"linux": "{PYPE_MODULE_ROOT}/setup/houdini:&",
"windows": "{PYPE_MODULE_ROOT}/setup/houdini;&"
"darwin": "{PYPE_ROOT}/setup/houdini:&",
"linux": "{PYPE_ROOT}/setup/houdini:&",
"windows": "{PYPE_ROOT}/setup/houdini;&"
},
"HOUDINI_MENU_PATH": {
"darwin": "{PYPE_MODULE_ROOT}/setup/houdini:&",
"linux": "{PYPE_MODULE_ROOT}/setup/houdini:&",
"windows": "{PYPE_MODULE_ROOT}/setup/houdini;&"
"darwin": "{PYPE_ROOT}/setup/houdini:&",
"linux": "{PYPE_ROOT}/setup/houdini:&",
"windows": "{PYPE_ROOT}/setup/houdini;&"
}
},
"variants": {
@ -924,9 +920,9 @@
"CREATE_NEW_CONSOLE"
]
},
"BLENDER_USER_SCRIPTS": "{PYPE_MODULE_ROOT}/repos/avalon-core/setup/blender",
"BLENDER_USER_SCRIPTS": "{PYPE_REPOS_ROOT}/avalon-core/setup/blender",
"PYTHONPATH": [
"{PYPE_MODULE_ROOT}/repos/avalon-core/setup/blender",
"{PYPE_REPOS_ROOT}/avalon-core/setup/blender",
"{PYTHONPATH}"
],
"CREATE_NEW_CONSOLE": "yes"
@ -1102,14 +1098,12 @@
"__environment_keys__": {
"photoshop": [
"AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH",
"PYTHONPATH",
"PYPE_LOG_NO_COLORS",
"WEBSOCKET_URL",
"WORKFILES_SAVE_AS"
]
},
"AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH": "1",
"PYTHONPATH": "{PYTHONPATH}",
"PYPE_LOG_NO_COLORS": "Yes",
"WEBSOCKET_URL": "ws://localhost:8099/ws/",
"WORKFILES_SAVE_AS": "Yes"
@ -1168,14 +1162,12 @@
"__environment_keys__": {
"aftereffects": [
"AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH",
"PYTHONPATH",
"PYPE_LOG_NO_COLORS",
"WEBSOCKET_URL",
"WORKFILES_SAVE_AS"
]
},
"AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH": "1",
"PYTHONPATH": "{PYTHONPATH}",
"PYPE_LOG_NO_COLORS": "Yes",
"WEBSOCKET_URL": "ws://localhost:8097/ws/",
"WORKFILES_SAVE_AS": "Yes"
@ -1236,7 +1228,7 @@
"CELACTION_TEMPLATE"
]
},
"CELACTION_TEMPLATE": "{PYPE_MODULE_ROOT}/pype/hosts/celaction/celaction_template_scene.scn"
"CELACTION_TEMPLATE": "{PYPE_ROOT}/pype/hosts/celaction/celaction_template_scene.scn"
},
"variants": {
"celation_Local": {
@ -1284,7 +1276,7 @@
"QT_PREFERRED_BINDING"
]
},
"AVALON_UNREAL_PLUGIN": "{PYPE_MODULE_ROOT}/repos/avalon-unreal-integration",
"AVALON_UNREAL_PLUGIN": "{PYPE_REPOS_ROOT}/avalon-unreal-integration",
"PYPE_LOG_NO_COLORS": "True",
"QT_PREFERRED_BINDING": "PySide"
},

View file

@ -15,9 +15,6 @@
"__environment_keys__": {
"global": [
"FFMPEG_PATH",
"PATH",
"PYTHONPATH",
"PYPE_PROJECT_CONFIGS",
"PYPE_PYTHON_EXE",
"PYPE_OCIO_CONFIG",
"PYBLISH_GUI",
@ -26,19 +23,9 @@
},
"FFMPEG_PATH": {
"windows": "{PYPE_ROOT}/vendor/bin/ffmpeg_exec/windows/bin",
"darwin": "{VIRTUAL_ENV}/localized/ffmpeg_exec/darwin/bin:{PYPE_SETUP_PATH}/vendor/bin/ffmpeg_exec/darwin/bin",
"linux": "{VIRTUAL_ENV}/localized/ffmpeg_exec/linux:{PYPE_SETUP_PATH}/vendor/bin/ffmpeg_exec/linux"
"darwin": "{PYPE_ROOT}/vendor/bin/ffmpeg_exec/darwin/bin",
"linux": ":{PYPE_ROOT}/vendor/bin/ffmpeg_exec/linux"
},
"PATH": [
"{FFMPEG_PATH}",
"{PATH}"
],
"PYTHONPATH": {
"windows": "{VIRTUAL_ENV}/Lib/site-packages;{PYPE_MODULE_ROOT}/pype/tools;{PYPE_MODULE_ROOT}/pype/vendor;{PYTHONPATH}",
"linux": "{VIRTUAL_ENV}/lib/python{PYTHON_VERSION}/site-packages:{PYPE_MODULE_ROOT}/pype/tools:{PYPE_MODULE_ROOT}/pype/vendor:{PYTHONPATH}",
"darwin": "{VIRTUAL_ENV}/lib/python{PYTHON_VERSION}/site-packages:{PYPE_MODULE_ROOT}/pype/tools:{PYPE_MODULE_ROOT}/pype/vendor:{PYTHONPATH}"
},
"PYPE_PROJECT_CONFIGS": "{PYPE_SETUP_PATH}/../studio-project-configs",
"PYPE_PYTHON_EXE": {
"windows": "{VIRTUAL_ENV}/Scripts/python.exe",
"linux": "{VIRTUAL_ENV}/Scripts/python",

View file

@ -61,7 +61,7 @@
}
},
"is_hierarchical": {
"avalon_mongo_id": {
"tools_env": {
"write_security_roles": [
"API",
"Administrator",
@ -73,7 +73,7 @@
"Pypeclub"
]
},
"tools_env": {
"avalon_mongo_id": {
"write_security_roles": [
"API",
"Administrator",

View file

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more