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