Can I use @route in shared code - and how?

I am reorganizing the code of my plugin - and as a result have moved some of it into the Contents/Services/Shared Code directory.

 

This now works fine, with one last crucial issue: I cannot use @route.

 

I.e., if I put the following in my shared code file:

@route('/video/mythrecordings/GetRecordingInfo', allow_sync=True)
def RecordingInfo(chanId, startTime, seriesInetRef = None):
	Log('RecordingInfo')
	...etc...

I get the following error in my log:

2015-03-05 12:14:29,286 (7f518affd700) :  DEBUG (services:362) - Loaded services
2015-03-05 12:14:29,290 (7f518a7fc700) :  DEBUG (services:433) - Loading 2 shared code modules
2015-03-05 12:14:29,399 (7f518a7fc700) :  CRITICAL (services:441) - Error loading shared code (most recent call last):
  File "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-ins/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/components/services.py", line 435, in _setup_shared_code_sandbox
    sandbox.execute(code)
  File "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-ins/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/code/sandbox.py", line 256, in execute
    exec(code) in self.environment
  File "", line 2, in 
  File "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-ins/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/code/sandbox.py", line 333, in __import__
    return mod.load_module(_name)
  File "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-ins/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/code/sandbox.py", line 44, in load_module
    module = RestrictedModule(name, path, sandbox)
  File "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-ins/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/code/loader.py", line 30, in __init__
    exec(code) in self.__dict__
  File "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-ins/MythRecordings.bundle/Contents/Services/Shared Code/plexmythui.pys", line 257, in 
    @route('/video/mythrecordings/GetRecordingInfo', allow_sync=True)
NameError: name 'route' is not defined

If I remove the @route, the error goes away, and the code works - but I'm unable to sync anymore.

 

I probably just need to import something, but my Python knowledge is to feeble to figure it out.

 

I tried from routes import route, which I found in the Framework bundle - put that went totally pear shaped.

 

Looks like you've run into some sandbox restrictions. I think the Shared code isn't meant to be "routable" and there are even more restrictions, for example, you can't pass a callback to a function from the "general" code into shared code.

My advice would be to think of a `Code/__init__.py` as of a Controller and offload the "business logic" (parsing pages, etc) into the Shared code services (alias Models). This way the controller can be kept relatively simple.

Yep exactly what was said above.

 @gerk, @czukowski

 
Thanks for answering - will raising the privilege level to Elevated help, perhaps?
 
If not, I'm eager to follow your advice - but I don't know enough about the execution model to know exactly how best to do that (as my current problem illustrates).
 
So I thought I'd present the problem in a simplified form, and ask for advice on how to structure my code
 
Let me get specific on you:
 
 
The plugin
 
The plugin lets you browse and play recorded TV from your MythTV server. Below I'll present the structural variants I've been going through - maybe you can see a better way?
 
 
Baseline: monolithic
 
The currently publiched version is structured as follows (all the Python code in the __init__.py file):
Start() ==> GetRecordingList(sortBy) ==> GetMythRecordings() --------> MythTV services API

The MythTV services API is a REST-like API running on port 6544 of any MythTV backend. It has a GetRecordings REST call that returns metadata for all the recordings on the server, as XML.

 
The GetMythRecordings() Python function retrieves this XML and turns it into a list of XML objects (really just a pimped-up version of XML.ElementFromUrl()).
 
GetRecordingList() takes this this list of XML objects and turns it into a Plex ObjectContainer with a VideoClipObject for each recording.
 
 
Variant 1: shared "Model" module
 
Now, I want to add a search service. So - along the lines of your advice - I moved the GetMythRecordings into shared "model" code (called plexmythlib.pys), which is called from both the plugin (__init__.py) and the search service (ServiceCode.pys):
__init__.py:     Start() ==> GetRecordingList(sortBy) 
plexmythlib.pys:                                      ==> GetMythRecordings() -----> MythTV services API
ServiceCode.pys: Search() ==> GetRecordingList(query) 

But doing this, I realized that the two versions of GetRecordingList() were 95% identical!

 
 
Variant 2: shared "UI" module
 
So this is where I thought I'd move GetRecordingList into shared code too (in a plexmythui.pys module):
__init__.py:     Start()  
                           ==> plexmythui.pys:GetRecordingList(sortBy, query) ==> plexmythlib.pys:GetMythRecordings() --> MythTV services API
ServiceCode.pys: Search()  
 
The problem
 
GetRecordingList looks very roughly like this:
def GetRecordingList(sortBy, query):
   recordings = plexmythlib.GetMythRecordings()
   ...sort and filter...
   oc = ObjectContainer(...)
   for recording in recordings:
      oc.add(Recording(recording))
   return oc

def Recording(recording):
   return VideoClipObject(
             key = Callback(RecordingInfo, chanId=chanId, startTime=recordingStart),
             items = [MediaObject(…)]
          )

@route(’/video/mythrecordings/GetRecordingInfo’, allow_sync=True) 
def RecordingInfo(chanId, startTime):
   …find recording based on chanId and startTime…
   recording_object = Recording(recording)
   return ObjectContainer(objects=[recording_object]

The problem is that GetRecordingInfo apparently needs to be there in order for sync to work - as does the @route(...allow_sync_True)!

 
So I'm stuck between skipping variant 2 (with the resulting duplicated code), or removing the @route (which means giving up support for PlexSync, on of my favourite features).
 
 
The question
 
How can I structure my code to share the "UI" code in GetRecordingList(), Recording() and RecordingInfo()?

I don't think elevated privileges will help, but you're welcome to try, that should be simple :)

Anyway, here's what I would change in your code:

def GetRecordingList(sortBy, query):
   oc = ObjectContainer(...)
   recordings = plexmythlib.GetMythRecordings(sortBy, query) # let the shared code do sorting and filtering
   for recording in recordings:
      oc.add(Recording(recording))
   return oc

def Recording(recording):
   return VideoClipObject(
             key = Callback(RecordingInfo, chanId=chanId, startTime=recordingStart),
             items = [MediaObject(…)]
          )

@route(’/video/mythrecordings/GetRecordingInfo’, allow_sync=True) 
def RecordingInfo(chanId, startTime):
recording = plexmythlib.GetMythRecording(chanId, startTime) # let the shared code get the recording info
   recording_object = Recording(recording)
   return ObjectContainer(objects=[recording_object]

Now the only thing you're taking care of in the controller (probably would be more correct to call it "presenter" but it doesn't matter much...) is composing some media objects and containers from the data received from the model.

You'll have to come up with some intermediate data structure because you can't just pass a callback to Recording function into the model and make it create a VideoClipObject. It is an inconvenience, but given that the code will be much shorter and cleaner and you know it only takes care of one job ("single responsibility principle"), I'd say it's worth it.

Here's an example of the one of my channels `__init__.py`: https://bitbucket.org/czukowski/plex-svoeradio/src/10d2630f41a9936c9b0164f5e2285adf74df13ef/Code/__init__.py?at=master (the model it calls is much less readable, but still way better than if it was combined in one file).

@czukowski, @Gerk
 
I got interrupted by a power cut that crashed my server (total re-install - thank the Gods of Code for backups and github!), so this has taken a while.
 
Anyways, I've got a success to report, in case others run into this.
 
But first to your suggestion: I totally agree - in fact the separation of all the server-interaction code into plexmythlib was the first thing I went about doing. Total success, separation of concerns, smiles all round.
 
But...
 
There is a LOT of code between the data returned from plexmythlib.GetMythRecordings and actually populating the VideoCkipObject. 
 
One example would be this: I check any differences between the scheduled start of the program and the actual recording start, and put a "WARNING: Recording may have missed start of program!" in the start of the summary. And that's just the start of it...
 
And all this code (many hundreds of lines) is something I would like to share between the main channel code (__init__.py) and the search service (ServiceCode.pys).
 
So my problem remained: how to move the formatting code - which relies heavily on @route and Callback(...) - into shared code?
 
Here's my solution:
 
Shared code (plexmythui.pys):
def RecordingListHelper(..., MakeRecordingInfoUrl):
   ...
   oc.add(VideoClipObject(key=MakeRecordingInfoUrl(chanId=chanId, startTime=recordingStart))
   ...
   return oc

def RecoringInfoHelper(chanId, startTime):
   …

Calling code (__init__.py):

GetRecordingListHelper = SharedCodeService.plexmythui.GetRecordingListHelper
RecordingInfoHelper = SharedCodeService.plexmythui.RecordingInfoHelper

def MainMenu():
   dir=ObjectContainer()
   dir.Add(DirectoryObject(key=Callback(GetRecordingList, 

RecordingList:

def GetRecordingList(…):
   return GetRecordingListHelper(…, MakeRecordingInfoUrl)

RecordingInfo:

@route(’/video/mythrecordings/GetRecordingInfo’, allow_sync=True)
def RecordingInfo(chanId, startTime):
return RecordingInfoHelper(chanId, startTime, MakeRecordingInfoURL)

def MakeRecordingInfoURL(chanId, startTime):
return Callback(RecordingInfo, chanId=chanId, startTime=startTime)

The core trick is obviously passing the MakeRecordingInfoURL callback into the shared code.

I'm not sure if it's prettier, but it's at least an alternative :-)

But thanks for your help, both - it saved me a lot of time examining dead ends, and got my thoughts moving in new directions

Last I checked (ok, I checked it just once when I was starting with channels, almost a year ago), passing a callback into shared code, even though it looked as a logical step, caused sandbox violations, so this won't work:

Shared code (plexmythui.pys):

def RecordingListHelper(..., MakeRecordingInfoUrl):
   ...
   oc.add(VideoClipObject(key=MakeRecordingInfoUrl(chanId=chanId, startTime=recordingStart))

Researching options how to break out of sandbox didn't seem a good idea to me, so I had to resort to the solution I've mentioned above.

Example from your channel:

# RecordingList:
def GetRecordingList(...):
    list = ObjectContainer()
    # this returns a simple data structure with all the info required .
    recordings = SharedCodeService.plexmythui.GetRecordingList(...)
    # iterate through the list...
    for recording in recordings:
        # create a media object for each item, no fancy logic here, string translation and callback for `key` at most
        list.add(VideoClipObject(
            key=Callback(RecordingInfo, chanId=recording['channel_id'], startTime=recording['start_time']),
            title=recording['title'],
            # as mentioned above, let the shared code compare start/record times and compose a summary text
            summary=recording['summary'],
            thumb=recording['icon']
            # and so on...
        ))
    return list

It even makes sense from the architectural view: models should not have any knowledge of the controllers that use them. Ideally you should be able to run your model from the Python command prompt, such as:

from plexmythui.pys import GetRecordingList
print GetRecordingList()

(yes, that's exaggeration as there's Plex Framework's magically injected HTTP and other similar classes that you'll almost certainly use in the shared code anyway, that was just to illustrate the point)