Merge branch 'devel' of dev.sourcefabric.org:airtime into devel
This commit is contained in:
commit
f717efd7fb
|
@ -240,33 +240,6 @@ class Application_Model_StoredFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get one metadata value.
|
|
||||||
*
|
|
||||||
* @param string $p_category (MDATA_KEY_URL)
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getMetadataValue($p_category)
|
|
||||||
{
|
|
||||||
// constant() was used because it gets quoted constant name value from
|
|
||||||
// api_client.py. This is the wrapper funtion
|
|
||||||
return $this->getDbColMetadataValue(constant($p_category));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get one metadata value.
|
|
||||||
*
|
|
||||||
* @param string $p_category (url)
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getDbColMetadataValue($p_category)
|
|
||||||
{
|
|
||||||
$propelColumn = $this->_dbMD[$p_category];
|
|
||||||
$method = "get$propelColumn";
|
|
||||||
|
|
||||||
return $this->_file->$method();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get metadata as array, indexed by the column names in the database.
|
* Get metadata as array, indexed by the column names in the database.
|
||||||
*
|
*
|
||||||
|
@ -423,7 +396,6 @@ SQL;
|
||||||
return $possible_ext;
|
return $possible_ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// We fallback to guessing the extension from the mimetype if we
|
// We fallback to guessing the extension from the mimetype if we
|
||||||
// cannot extract it from the file name
|
// cannot extract it from the file name
|
||||||
|
|
||||||
|
@ -1020,15 +992,18 @@ SQL;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Enter description here ...
|
* Enter description here ...
|
||||||
* @param $dir_id - if this is not provided, it returns all files with full path constructed.
|
* @param $dir_id - if this is not provided, it returns all files with full
|
||||||
|
* path constructed.
|
||||||
*/
|
*/
|
||||||
public static function listAllFiles($dir_id=null, $all)
|
public static function listAllFiles($dir_id=null, $all)
|
||||||
{
|
{
|
||||||
$con = Propel::getConnection();
|
$con = Propel::getConnection();
|
||||||
|
|
||||||
$sql = "SELECT filepath as fp"
|
$sql = <<<SQL
|
||||||
." FROM CC_FILES as f"
|
SELECT filepath AS fp
|
||||||
." WHERE f.directory = :dir_id";
|
FROM CC_FILES AS f
|
||||||
|
WHERE f.directory = :dir_id
|
||||||
|
SQL;
|
||||||
|
|
||||||
if (!$all) {
|
if (!$all) {
|
||||||
$sql .= " AND f.file_exists = 'TRUE'";
|
$sql .= " AND f.file_exists = 'TRUE'";
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
<?php if (count($watched_dirs) > 0): ?>
|
<?php if (count($watched_dirs) > 0): ?>
|
||||||
<?php foreach($watched_dirs as $watched_dir): ?>
|
<?php foreach($watched_dirs as $watched_dir): ?>
|
||||||
<dd class="block-display selected-item">
|
<dd class="block-display selected-item">
|
||||||
<?php echo ($watched_dir->getExistsFlag())?"":"<span class='ui-icon-alert'><img src='/css/images/warning-icon.png'></span>"?><span id="folderPath"><?php echo $watched_dir->getDirectory();?></span></span>
|
<?php echo ($watched_dir->getExistsFlag())?"":"<span class='ui-icon-alert'><img src='/css/images/warning-icon.png'></span>"?><span id="folderPath" style="display:block; width:350px"><?php echo $watched_dir->getDirectory();?></span></span>
|
||||||
<span title="Rescan watched directory (This is useful if it is network mount and may be out of sync with Airtime)" class="ui-icon ui-icon-refresh"></span>
|
<span title="Rescan watched directory (This is useful if it is network mount and may be out of sync with Airtime)" class="ui-icon ui-icon-refresh"></span>
|
||||||
<span title="Remove watched directory" class="ui-icon ui-icon-close"></span>
|
<span title="Remove watched directory" class="ui-icon ui-icon-close"></span>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div id="library_content" class="lib-content tabs ui-widget ui-widget-content block-shadow alpha-block padded">
|
<div id="library_content" class="lib-content tabs ui-widget ui-widget-content block-shadow alpha-block padded">
|
||||||
<div id="import_status" style="display:none">File import in progress...</div>
|
<div id="import_status" style="display:none">File import in progress...</div>
|
||||||
<div id="advanced_search" class="advanced_search form-horizontal"></div>
|
<fieldset class="toggle" id="filter_options">
|
||||||
|
<legend style="cursor: pointer;"><span class="ui-icon ui-icon-triangle-2-n-s"></span>Advanced Search Options</legend>
|
||||||
|
<div id="advanced_search" class="advanced_search form-horizontal"></div>
|
||||||
|
</fieldset>
|
||||||
<table id="library_display" cellpadding="0" cellspacing="0" class="datatable"></table>
|
<table id="library_display" cellpadding="0" cellspacing="0" class="datatable"></table>
|
||||||
</div>
|
</div>
|
||||||
<div id="show_builder" class="sb-content ui-widget ui-widget-content block-shadow omega-block padded">
|
<div id="show_builder" class="sb-content ui-widget ui-widget-content block-shadow omega-block padded">
|
||||||
|
|
|
@ -38,6 +38,7 @@ var AIRTIME = (function(AIRTIME) {
|
||||||
"track_num" : "n",
|
"track_num" : "n",
|
||||||
"year" : "n",
|
"year" : "n",
|
||||||
"owner_id" : "s",
|
"owner_id" : "s",
|
||||||
|
"info_url" : "s",
|
||||||
"replay_gain" : "n"
|
"replay_gain" : "n"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1041,6 +1042,7 @@ function addQtipToSCIcons(){
|
||||||
*/
|
*/
|
||||||
function validateAdvancedSearch(divs) {
|
function validateAdvancedSearch(divs) {
|
||||||
var valid = true,
|
var valid = true,
|
||||||
|
allValid = true,
|
||||||
fieldName,
|
fieldName,
|
||||||
fields,
|
fields,
|
||||||
searchTerm = Array(),
|
searchTerm = Array(),
|
||||||
|
@ -1053,7 +1055,6 @@ function validateAdvancedSearch(divs) {
|
||||||
|
|
||||||
searchTerm[0] = "";
|
searchTerm[0] = "";
|
||||||
searchTerm[1] = "";
|
searchTerm[1] = "";
|
||||||
|
|
||||||
$.each(divs, function(i, div){
|
$.each(divs, function(i, div){
|
||||||
fieldName = $(div).children(':nth-child(2)').attr('id');
|
fieldName = $(div).children(':nth-child(2)').attr('id');
|
||||||
fields = $(div).children().find('input');
|
fields = $(div).children().find('input');
|
||||||
|
@ -1085,6 +1086,7 @@ function validateAdvancedSearch(divs) {
|
||||||
//string fields do not need validation
|
//string fields do not need validation
|
||||||
if (searchTermType !== "s") {
|
if (searchTermType !== "s") {
|
||||||
valid = regExpr.test(searchTerm[i]);
|
valid = regExpr.test(searchTerm[i]);
|
||||||
|
if (!valid) allValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
addRemoveValidationIcons(valid, $(field));
|
addRemoveValidationIcons(valid, $(field));
|
||||||
|
@ -1107,13 +1109,9 @@ function validateAdvancedSearch(divs) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return valid;
|
return allValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRemoveValidationIcons(valid, field) {
|
function addRemoveValidationIcons(valid, field) {
|
||||||
|
|
|
@ -428,6 +428,12 @@ var AIRTIME = (function(AIRTIME){
|
||||||
if (isStatic) {
|
if (isStatic) {
|
||||||
$.each(data, function(index, ele){
|
$.each(data, function(index, ele){
|
||||||
if (ele.track_title !== undefined) {
|
if (ele.track_title !== undefined) {
|
||||||
|
if (ele.creator === null) {
|
||||||
|
ele.creator = "";
|
||||||
|
}
|
||||||
|
if (ele.track_title === null) {
|
||||||
|
ele.track_title = "";
|
||||||
|
}
|
||||||
$html += "<li>" +
|
$html += "<li>" +
|
||||||
"<span class='block-item-title'>"+ele.track_title+" - </span>" +
|
"<span class='block-item-title'>"+ele.track_title+" - </span>" +
|
||||||
"<span class='block-item-author'>"+ele.creator+"</span>" +
|
"<span class='block-item-author'>"+ele.creator+"</span>" +
|
||||||
|
|
|
@ -2,6 +2,45 @@ from media.monitor.log import Loggable
|
||||||
from media.monitor.events import DeleteFile
|
from media.monitor.events import DeleteFile
|
||||||
|
|
||||||
class EventContractor(Loggable):
|
class EventContractor(Loggable):
|
||||||
|
def __init__(self):
|
||||||
|
self.store = {}
|
||||||
|
def event_registered(self, evt):
|
||||||
|
"""
|
||||||
|
returns true if the event is registered which means that there is
|
||||||
|
another "unpacked" event somewhere out there with the same path
|
||||||
|
"""
|
||||||
|
return evt.path in self.store
|
||||||
|
|
||||||
|
def get_old_event(self, evt):
|
||||||
|
"""
|
||||||
|
get the previously registered event with the same path as 'evt'
|
||||||
|
"""
|
||||||
|
return self.store[ evt.path ]
|
||||||
|
|
||||||
|
def register(self, evt):
|
||||||
|
if self.event_registered(evt):
|
||||||
|
ev_proxy = self.get_old_event(evt)
|
||||||
|
if ev_proxy.same_event(evt):
|
||||||
|
ev_proxy.merge_proxy(evt)
|
||||||
|
return False
|
||||||
|
# delete overrides any other event
|
||||||
|
elif evt.is_event(DeleteFile):
|
||||||
|
ev_proxy.merge_proxy(evt)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
ev_proxy.run_hook()
|
||||||
|
ev_proxy.reset_hook()
|
||||||
|
|
||||||
|
self.store[ evt.path ] = evt
|
||||||
|
evt.set_pack_hook( lambda : self.__unregister(evt) )
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __unregister(self, evt):
|
||||||
|
del self.store[evt.path]
|
||||||
|
|
||||||
|
|
||||||
|
# Delete this class when done using
|
||||||
|
class EventContractor2(Loggable):
|
||||||
"""
|
"""
|
||||||
This class is responsible for "contracting" events together to ease the
|
This class is responsible for "contracting" events together to ease the
|
||||||
load on airtime. It does this by morphing old events into newer ones
|
load on airtime. It does this by morphing old events into newer ones
|
||||||
|
|
|
@ -42,6 +42,43 @@ class EventRegistry(object):
|
||||||
raise Exception("You can instantiate this class. Must only use class \
|
raise Exception("You can instantiate this class. Must only use class \
|
||||||
methods")
|
methods")
|
||||||
|
|
||||||
|
|
||||||
|
class EventProxy(object):
|
||||||
|
"""
|
||||||
|
A container object for instances of BaseEvent (or it's subclasses) used for
|
||||||
|
event contractor
|
||||||
|
"""
|
||||||
|
def __init__(self, orig_evt):
|
||||||
|
self.orig_evt = orig_evt
|
||||||
|
self.evt = orig_evt
|
||||||
|
self.reset_hook()
|
||||||
|
if hasattr(orig_evt, 'path'): self.path = orig_evt.path
|
||||||
|
|
||||||
|
def set_pack_hook(self, l):
|
||||||
|
self._pack_hook = l
|
||||||
|
|
||||||
|
def reset_hook(self):
|
||||||
|
self._pack_hook = lambda : None
|
||||||
|
|
||||||
|
def run_hook(self):
|
||||||
|
self._pack_hook()
|
||||||
|
|
||||||
|
def safe_pack(self):
|
||||||
|
self.run_hook()
|
||||||
|
# make sure that cleanup hook is never called twice for the same event
|
||||||
|
self.reset_hook()
|
||||||
|
return self.evt.safe_pack()
|
||||||
|
|
||||||
|
def merge_proxy(self, proxy):
|
||||||
|
self.evt = proxy.evt
|
||||||
|
|
||||||
|
def is_event(self, real_event):
|
||||||
|
return isinstance(self.evt, real_event)
|
||||||
|
|
||||||
|
def same_event(self, proxy):
|
||||||
|
return self.evt.__class__ == proxy.evt.__class__
|
||||||
|
|
||||||
|
|
||||||
class HasMetaData(object):
|
class HasMetaData(object):
|
||||||
"""
|
"""
|
||||||
Any class that inherits from this class gains the metadata attribute that
|
Any class that inherits from this class gains the metadata attribute that
|
||||||
|
@ -91,6 +128,9 @@ class BaseEvent(Loggable):
|
||||||
"""
|
"""
|
||||||
self._pack_hook = k
|
self._pack_hook = k
|
||||||
|
|
||||||
|
def proxify(self):
|
||||||
|
return EventProxy(self)
|
||||||
|
|
||||||
# As opposed to unsafe_pack...
|
# As opposed to unsafe_pack...
|
||||||
def safe_pack(self):
|
def safe_pack(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -490,7 +490,6 @@ def file_playable(pathname):
|
||||||
command = ("airtime-liquidsoap -c 'output.dummy" + \
|
command = ("airtime-liquidsoap -c 'output.dummy" + \
|
||||||
"(audio_to_stereo(single(\"%s\")))' > /dev/null 2>&1") % \
|
"(audio_to_stereo(single(\"%s\")))' > /dev/null 2>&1") % \
|
||||||
pathname.replace("'", "'\\''")
|
pathname.replace("'", "'\\''")
|
||||||
print(command)
|
|
||||||
return True
|
return True
|
||||||
return_code = subprocess.call(command, shell=True)
|
return_code = subprocess.call(command, shell=True)
|
||||||
return (return_code == 0)
|
return (return_code == 0)
|
||||||
|
|
|
@ -8,6 +8,7 @@ from media.monitor.log import Loggable
|
||||||
from media.monitor.exceptions import BadSongFile
|
from media.monitor.exceptions import BadSongFile
|
||||||
from media.monitor.pure import LazyProperty
|
from media.monitor.pure import LazyProperty
|
||||||
from media.monitor.eventcontractor import EventContractor
|
from media.monitor.eventcontractor import EventContractor
|
||||||
|
from media.monitor.events import EventProxy
|
||||||
|
|
||||||
import api_clients.api_client as ac
|
import api_clients.api_client as ac
|
||||||
|
|
||||||
|
@ -125,7 +126,7 @@ class WatchSyncer(ReportHandler,Loggable):
|
||||||
try:
|
try:
|
||||||
# If there is a strange bug anywhere in the code the next line
|
# If there is a strange bug anywhere in the code the next line
|
||||||
# should be a suspect
|
# should be a suspect
|
||||||
if self.contractor.register(event): self.push_queue( event )
|
if self.contractor.register(EventProxy(event)): self.push_queue( event )
|
||||||
#self.push_queue( event )
|
#self.push_queue( event )
|
||||||
except BadSongFile as e:
|
except BadSongFile as e:
|
||||||
self.fatal_exception("Received bas song file '%s'" % e.path, e)
|
self.fatal_exception("Received bas song file '%s'" % e.path, e)
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
# The tests rely on a lot of absolute paths and other garbage so this file
|
# The tests rely on a lot of absolute paths and other garbage so this file
|
||||||
# configures all of that
|
# configures all of that
|
||||||
|
|
||||||
music_folder = u'/home/rudi/music'
|
music_folder = u'/home/rudi/music'
|
||||||
o_path = u'/home/rudi/throwaway/ACDC_-_Back_In_Black-sample-64kbps.ogg'
|
o_path = u'/home/rudi/throwaway/ACDC_-_Back_In_Black-sample-64kbps.ogg'
|
||||||
watch_path = u'/home/rudi/throwaway/fucking_around/watch/',
|
watch_path = u'/home/rudi/throwaway/fucking_around/watch/',
|
||||||
real_path1 = u'/home/rudi/throwaway/fucking_around/watch/unknown/unknown/ACDC_-_Back_In_Black-sample-64kbps-64kbps.ogg'
|
real_path1 = u'/home/rudi/throwaway/fucking_around/watch/unknown/unknown/ACDC_-_Back_In_Black-sample-64kbps-64kbps.ogg'
|
||||||
opath = u"/home/rudi/Airtime/python_apps/media-monitor2/tests/"
|
opath = u"/home/rudi/Airtime/python_apps/media-monitor2/tests/"
|
||||||
ppath = u"/home/rudi/Airtime/python_apps/media-monitor2/media/"
|
ppath = u"/home/rudi/Airtime/python_apps/media-monitor2/media/"
|
||||||
sample_config = u'/home/rudi/Airtime/python_apps/media-monitor2/tests/api_client.cfg'
|
|
||||||
real_config = u'/home/rudi/Airtime/python_apps/media-monitor2/tests/live_client.cfg'
|
|
||||||
api_client_path = '/etc/airtime/api_client.cfg'
|
api_client_path = '/etc/airtime/api_client.cfg'
|
||||||
|
# holdover from the time we had a special config for testing
|
||||||
if __name__ == "__main__":
|
sample_config = api_client_path
|
||||||
shutil.copy(api_client_path, real_config)
|
real_config = api_client_path
|
||||||
# TODO : fix this to use liberal permissions
|
|
||||||
subprocess.call(["chown","rudi",real_config])
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@ import prepare_tests
|
||||||
|
|
||||||
class TestApiClient(unittest.TestCase):
|
class TestApiClient(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
test_path = prepare_tests.real_config
|
test_path = prepare_tests.api_client_path
|
||||||
|
print("Running from api_config: %s" % test_path)
|
||||||
if not os.path.exists(test_path):
|
if not os.path.exists(test_path):
|
||||||
print("path for config does not exist: '%s' % test_path")
|
print("path for config does not exist: '%s' % test_path")
|
||||||
# TODO : is there a cleaner way to exit the unit testing?
|
# TODO : is there a cleaner way to exit the unit testing?
|
||||||
|
|
|
@ -3,48 +3,37 @@ from media.monitor.eventcontractor import EventContractor
|
||||||
#from media.monitor.exceptions import BadSongFile
|
#from media.monitor.exceptions import BadSongFile
|
||||||
from media.monitor.events import FakePyinotify, NewFile, MoveFile, \
|
from media.monitor.events import FakePyinotify, NewFile, MoveFile, \
|
||||||
DeleteFile
|
DeleteFile
|
||||||
from mock import patch
|
|
||||||
|
|
||||||
class TestMMP(unittest.TestCase):
|
class TestMMP(unittest.TestCase):
|
||||||
def test_event_registered(self):
|
def test_event_registered(self):
|
||||||
ev = EventContractor()
|
ev = EventContractor()
|
||||||
e1 = NewFile( FakePyinotify('bullshit.mp3') )
|
e1 = NewFile( FakePyinotify('bullshit.mp3') ).proxify()
|
||||||
e2 = MoveFile( FakePyinotify('bullshit.mp3') )
|
e2 = MoveFile( FakePyinotify('bullshit.mp3') ).proxify()
|
||||||
ev.register(e1)
|
ev.register(e1)
|
||||||
self.assertTrue( ev.event_registered(e2) )
|
self.assertTrue( ev.event_registered(e2) )
|
||||||
|
|
||||||
def test_get_old_event(self):
|
def test_get_old_event(self):
|
||||||
ev = EventContractor()
|
ev = EventContractor()
|
||||||
e1 = NewFile( FakePyinotify('bullshit.mp3') )
|
e1 = NewFile( FakePyinotify('bullshit.mp3') ).proxify()
|
||||||
e2 = MoveFile( FakePyinotify('bullshit.mp3') )
|
e2 = MoveFile( FakePyinotify('bullshit.mp3') ).proxify()
|
||||||
ev.register(e1)
|
ev.register(e1)
|
||||||
self.assertEqual( ev.get_old_event(e2), e1 )
|
self.assertEqual( ev.get_old_event(e2), e1 )
|
||||||
|
|
||||||
def test_register(self):
|
def test_register(self):
|
||||||
ev = EventContractor()
|
ev = EventContractor()
|
||||||
e1 = NewFile( FakePyinotify('bullshit.mp3') )
|
e1 = NewFile( FakePyinotify('bullshit.mp3') ).proxify()
|
||||||
e2 = DeleteFile( FakePyinotify('bullshit.mp3') )
|
e2 = DeleteFile( FakePyinotify('bullshit.mp3') ).proxify()
|
||||||
self.assertTrue( ev.register(e1) )
|
self.assertTrue( ev.register(e1) )
|
||||||
|
|
||||||
# Check that morph_into is called when it should be
|
|
||||||
with patch.object(NewFile, 'morph_into', return_value='kimchi') \
|
|
||||||
as mock_method:
|
|
||||||
ret = ev.register(e2)
|
|
||||||
self.assertFalse(ret)
|
|
||||||
mock_method.assert_called_once_with(e2)
|
|
||||||
|
|
||||||
# This time we are not patching morph
|
|
||||||
self.assertFalse( ev.register(e2) )
|
self.assertFalse( ev.register(e2) )
|
||||||
# We did not an element
|
|
||||||
self.assertTrue( len(ev.store.keys()) == 1 )
|
self.assertEqual( len(ev.store.keys()), 1 )
|
||||||
morphed = ev.get_old_event(e2)
|
|
||||||
self.assertTrue( isinstance(morphed, DeleteFile) )
|
|
||||||
|
|
||||||
delete_ev = e1.safe_pack()[0]
|
delete_ev = e1.safe_pack()[0]
|
||||||
self.assertEqual( delete_ev['mode'], u'delete')
|
self.assertEqual( delete_ev['mode'], u'delete')
|
||||||
self.assertTrue( len(ev.store.keys()) == 0 )
|
self.assertEqual( len(ev.store.keys()), 0 )
|
||||||
|
|
||||||
e3 = DeleteFile( FakePyinotify('horseshit.mp3') )
|
e3 = DeleteFile( FakePyinotify('horseshit.mp3') ).proxify()
|
||||||
self.assertTrue( ev.register(e3) )
|
self.assertTrue( ev.register(e3) )
|
||||||
self.assertTrue( ev.register(e2) )
|
self.assertTrue( ev.register(e2) )
|
||||||
|
|
||||||
|
@ -58,11 +47,12 @@ class TestMMP(unittest.TestCase):
|
||||||
DeleteFile( FakePyinotify(p) ),
|
DeleteFile( FakePyinotify(p) ),
|
||||||
NewFile( FakePyinotify(p) ),
|
NewFile( FakePyinotify(p) ),
|
||||||
NewFile( FakePyinotify(p) ), ]
|
NewFile( FakePyinotify(p) ), ]
|
||||||
|
events = map(lambda x: x.proxify(), events)
|
||||||
actual_events = []
|
actual_events = []
|
||||||
for e in events:
|
for e in events:
|
||||||
if ev.register(e):
|
if ev.register(e):
|
||||||
actual_events.append(e)
|
actual_events.append(e)
|
||||||
self.assertEqual( len(ev.store.keys()), 1 )
|
self.assertEqual( len(ev.store.keys()), 1 )
|
||||||
packed = [ x.safe_pack() for x in actual_events ]
|
#packed = [ x.safe_pack() for x in actual_events ]
|
||||||
|
|
||||||
if __name__ == '__main__': unittest.main()
|
if __name__ == '__main__': unittest.main()
|
||||||
|
|
Loading…
Reference in New Issue