1# -*- coding: utf-8 -*- 2"""The main classes needed by external scripts 3 4The Project class tracks a `remote.Project()` and `local.LocalFiles` objects 5and compares them using `sync.Changes()` 6 7Part of the pyosf package 8https://github.com/psychopy/pyosf/ 9 10Released under MIT license 11 12Created on Sun Feb 7 21:31:15 2016 13 14@author: lpzjwp 15""" 16 17from __future__ import absolute_import, print_function 18import os 19import sys 20import requests 21try: 22 from psychopy import logging 23except: 24 import logging 25from . import remote, local, sync 26import json 27 28PY3 = sys.version_info > (3,) 29 30 31class Project(object): 32 """Stores the project information for synchronization. 33 34 Stores the id and username for the remote.Project on OSF, the location of 35 the local files, and a record of the index at the point of previous sync 36 37 Parameters 38 ---------- 39 40 project_file : str 41 Location of the project file with info 42 43 root_path : str 44 The root of the folder where the local files are situated 45 46 osf : pyosf.remote.OSFProject instance) 47 The remote project that will be synchronised. 48 49 """ 50 def __init__(self, project_file=None, root_path=None, osf=None, 51 name='', autosave=True): 52 self.autosave = autosave # try to save file automatically on __del__ 53 self.project_file = project_file 54 self.root_path = root_path # overwrite previous (indexed) location 55 self.name = name # not needed but allows storing a short descr name 56 # these will be update from project file loading if it exists 57 self.index = [] 58 self.username = None 59 self.project_id = None 60 self.connected = False # have we gone online yet? 61 # load the project file (if exists) for info about previous sync 62 if project_file: 63 self.load(project_file) 64 65 # check/set root_path 66 if self.root_path is None: 67 self.root_path = root_path # use what we just loaded 68 elif root_path not in [None, self.root_path]: 69 logging.warn("The requested root_path and the previous root_path " 70 "differ. Using the requested path." 71 "given: {}" 72 "stored: {}".format(root_path, self.root_path)) 73 if self.root_path is None: 74 logging.warn("Project file failed to load a root_path " 75 "for the local files and none was provided") 76 77 self.osf = osf # the self.osf is as property set on-access 78 79 def __repr__(self): 80 return "Project({})".format(self.project_file) 81 82 def __del__(self): 83 if self.autosave: 84 self.save() 85 86 def save(self, proj_path=None): 87 """Save the project to a json-format file 88 89 The info will be: 90 - the `username` (so `remote.Project` can fetch an auth token) 91 - the project id 92 - the `root_path` 93 - the current files `index` 94 - a optional short `name` for the project 95 96 Parameters 97 ---------- 98 99 proj_path : str 100 Not needed unless saving to a new location. 101 102 """ 103 if proj_path is None: 104 proj_path = self.project_file 105 if not os.path.isdir(os.path.dirname(proj_path)): 106 os.makedirs(os.path.dirname(proj_path)) 107 if not os.path.isfile(proj_path): 108 logging.info("Creating new Project file: {}".format(proj_path)) 109 # create the fields to save 110 d = {} 111 d['root_path'] = self.root_path 112 d['name'] = self.name 113 d['username'] = self.username 114 d['project_id'] = self.project_id 115 d['index'] = self.index 116 # do the actual file save 117 with open(proj_path, 'wb') as f: 118 json_str = json.dumps(d, indent=2) 119 if PY3: 120 f.write(bytes(json_str, 'UTF-8')) 121 else: 122 f.write(json_str) 123 logging.info("Saved proj file: {}".format(proj_path)) 124 125 def load(self, proj_path=None): 126 """Load the project from a json-format file 127 128 The info will be: 129 - the `username` (so `remote.Project` can fetch an auth token) 130 - the project id the `root_path` 131 - the current files `index` 132 - a optional short `name` for the project 133 134 Parameters 135 ---------- 136 137 proj_path : str 138 Not needed unless saving to a new location. 139 140 Returns 141 ---------- 142 143 tuple (last_index, username, project_id, root_path) 144 145 """ 146 if proj_path is None: 147 proj_path = self.project_file 148 if proj_path is None: # STILL None: need to set later 149 return 150 elif not os.path.isfile(os.path.abspath(proj_path)): # path not found 151 logging.warn('No proj file: {}'.format(os.path.abspath(proj_path))) 152 else: 153 with open(os.path.abspath(proj_path), 'r') as f: 154 d = json.load(f) 155 self.username = d['username'] 156 self.index = d['index'] 157 self.project_id = d['project_id'] 158 self.root_path = d['root_path'] 159 if 'name' in d: 160 self.name = d['name'] 161 else: 162 self.name = '' 163 logging.info('Loaded proj: {}'.format(os.path.abspath(proj_path))) 164 165 def get_changes(self): 166 """Return the changes to be applied 167 """ 168 changes = sync.Changes(proj=self) 169 self.connected = True # we had to go online to get changes 170 return changes 171 172 @property 173 def osf(self): 174 """Get/sets the osf attribute. When 175 """ 176 if self._osf is None: 177 self.osf = self.project_id # go to setter using project_id 178 # if one of the above worked then self._osf should exist by now 179 return self._osf 180 181 @osf.setter 182 def osf(self, project): 183 if isinstance(project, remote.OSFProject): 184 self._osf = project 185 self.username = self._osf.session.username 186 self.project_id = self._osf.id 187 elif self.username is None: # if no project then we need username 188 raise AttributeError("No osf project was provided but also " 189 "no username or authentication token: {}" 190 .format(project)) 191 else: # with username create session and then project 192 try: 193 session = remote.Session(self.username) 194 except requests.exceptions.ConnectionError: 195 self._osf = None 196 self.connected = False 197 return 198 if self.project_id is None: 199 raise AttributeError("No project id was available. " 200 "Project needs OSFProject or a " 201 "previous project_file" 202 .format(project)) 203 else: 204 self._osf = remote.OSFProject(session=session, 205 id=self.project_id) 206 self.connected = True 207 208 @property 209 def root_path(self): 210 return self.__dict__['root_path'] 211 212 @root_path.setter 213 def root_path(self, root_path): 214 self.__dict__['root_path'] = root_path 215 if root_path is None: 216 self.local = None 217 else: 218 self.local = local.LocalFiles(root_path) 219