From 7e96ae62a14dab21f0f46d3828426f2f73a3cafb Mon Sep 17 00:00:00 2001 From: vsoch Date: Mon, 5 Jun 2017 23:55:24 -0400 Subject: [PATCH] adding most of watcher module, will write import_dicom celery job next --- README.md | 6 +- docs/watcher.md | 119 +++++++- sendit/apps/watcher/__init__.py | 0 sendit/apps/watcher/event_processors.py | 276 ++++++++++++++++++ sendit/apps/watcher/management/__init__.py | 0 .../watcher/management/commands/__init__.py | 0 .../management/commands/watcher_start.py | 98 +++++++ .../management/commands/watcher_stop.py | 41 +++ sendit/apps/watcher/signals.py | 38 +++ sendit/settings/watcher.py | 6 +- 10 files changed, 576 insertions(+), 8 deletions(-) create mode 100644 sendit/apps/watcher/__init__.py create mode 100644 sendit/apps/watcher/event_processors.py create mode 100644 sendit/apps/watcher/management/__init__.py create mode 100644 sendit/apps/watcher/management/commands/__init__.py create mode 100644 sendit/apps/watcher/management/commands/watcher_start.py create mode 100644 sendit/apps/watcher/management/commands/watcher_stop.py create mode 100644 sendit/apps/watcher/signals.py diff --git a/README.md b/README.md index 70a3c49..333bc9e 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,8 @@ This application lives in a docker-compose application running on `STRIDE-HL71`. This initial setup is stupid in that it's going to be checking an input folder to find new images. We do this using the [watcher](sendit/apps/watcher) application, which is started and stopped with a manage.py command: ``` -python manage.py start_watcher -python manage.py stop_watcher +python manage.py watcher_start +python manage.py watcher_stop ``` And the default is to watch for files added to [data](data), which is mapped to '/data' in the container. This means that `STRIDE-HL71` will receive DICOM from somewhere. It should use an atomic download strategy, but with folders, into the application data input folder. This will mean that when it starts, the folder might look like: @@ -118,7 +118,7 @@ Only when all of the dicom files are finished copying will the driving function ``` -For more details about the water daemon, you can look at [his docs](docs/watcher.md). +A directory is considered "finished" and ready for processing when it does **not** have an entension that starts with "tmp". For more details about the watcher daemon, you can look at [his docs](docs/watcher.md). While many examples are provided, for this application we use the celery task `import_dicomdir` in [main/tasks.py](sendit/apps/main/tasks.py) to read in a finished dicom directory from the directory being watched, and this uses the class `DicomCelery` in the [event_processors](sendit/apps/watcher/event_processors.py) file. Other examples are provided, in the case that you want to change or extend the watcher daemon. ### 2. Database Models diff --git a/docs/watcher.md b/docs/watcher.md index 8446425..f28a400 100644 --- a/docs/watcher.md +++ b/docs/watcher.md @@ -2,11 +2,88 @@ The watcher is implemented as a [pyinotify daemon](https://github.com/seb-m/pyinotify/wiki) that is controlled via the [manage.py](../manage.py). If you are more interested about how this module works, it uses [inotify](https://pypi.python.org/pypi/inotify) that comes from the linux Kernel. Specifically, this means that you can start and stop the daemon with the following commands (from inside the Docker image): ``` -python manage.py start_watcher -python manage.py stop_watcher +python manage.py watcher_start +python manage.py watcher_stop +``` + +When you start, you will see something like the following: + +``` +python manage.py watcher_start +DEBUG Adding watch on /data, processed by sendit.apps.watcher.event_processors.AllEventsSignaler +``` + +and you will notice a `pid` file for the watcher generated: + +``` +cat sendit/watcher.pid +142 +``` + +along with the error and output logs: + +``` +ls sendit/logs +watcher.err watcher.out +``` + +Then when you stop it, the pid file will go away (but the error and output logs remain) + +``` +python manage.py watcher_stop +DEBUG Dicom watching has been stopped. +``` + +## Configuration +Configuration for the watcher is stored in [settings/watcher.py](../sendit/settings/watcher.py). + + +### Logging +How did those files know to be generated under `logs`? We told them to be. Specifically, in this [settings/watcher.py](../sendit/settings/watcher.py) we define the locations for the output and error files, along with the `PID` file to store the daemon process ID: + +``` +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..')) +LOG_DIR = os.path.join(BASE_DIR,'logs') +INOTIFIER_DAEMON_STDOUT = os.path.join(LOG_DIR,'watcher.out') +INOTIFIER_DAEMON_STDERR = os.path.join(LOG_DIR,'watcher.err') +``` + +`BASE_DIR` refers to the `/code/sendit` directory, meaning that our logs will be found in `/code/sendit/logs`, and out daemon by default will be generated as a file called `watcher.pid` under `/code/sendit`. If there is, for any reason, an error with these paths, the same files will be generated in `/tmp`. + +### Watch Paths +A "watch path" setup can be thought of as one or more tuples, each including: + + - an actual folder to watch + - the level of events to watch, based on [pyinotify events](https://github.com/seb-m/pyinotify/wiki/Events-types) types + - the signaler to use in our application, currently defined in [watcher.event_processors](../sendit.apps.watcher.event_processors.AllEventsSignaler) + +Thus, if you want to change some function, whether that be the kind of event responded to, how the response is handled, or what locations are watched, you can do that by tweaking any of the above three things. For our settings file, we use a function to return a list of tuples: + +```python +# Logging +INOTIFIER_WATCH_PATHS = generate_watch_paths() +``` + +and the function `generate_watch_paths` is defined as follows: + +``` +def generate_watch_paths(): + '''Don't import pyinotify directly into settings + ''' + import pyinotify + return ( + ( os.path.join('/data'), + pyinotify.ALL_EVENTS, + 'sendit.apps.watcher.event_processors.DicomCelery', + ), + ) ``` +Note that this example returns just one, however you could return multiple, meaning that you have three folders with different events / responses setup: + + +## How does it work? The functions are stored in the [management/commands](../sendit/apps/watcher/management/commands) folder within the watcher app. This organization is standard for adding a command to `manage.py`, and is done by way of instantiating Django's [BaseCommand](https://docs.djangoproject.com/en/1.11/howto/custom-management-commands/#django.core.management.BaseCommand): ``` @@ -16,7 +93,45 @@ from django.core.management.base import ( ) ``` +When the user starts the daemon, after doing the appropriate checks to start logging and error files, the daemon starts `pyinotify` to watch the folder. Each time a file event is triggered that matches the settings given to `pyinotify`, the event is sent to the function specified (the third argument in the tuple) defined in [event_processors](../sendit/apps/watcher/event_processors.py). If you are curious, an event looks like this: + +```python +event.__dict__ + + { + 'name': 'hello', + 'maskname': 'IN_CLOSE_NOWRITE|IN_ISDIR', + 'wd': 1, + 'dir': True, + 'path': '/data', + 'pathname': '/data/hello', + 'mask': 1073741840} + } + +``` + +It is then up to the function in `event_processors` to decide, based on the event. For example, if you use the `AllEventsSignaler` class, a proper signal defined in [signals.py](../sendit/apps/watcher/signals.py) will be fired (this is not currently in use, but could be useful if needed). It would look something like this for something like accessing a file: + +```python + +# In event_processors.py +class AllEventsSignaler(pyinotify.ProcessEvent): + '''Example class to signal on every event. + ''' + def process_IN_ACCESS(self, event): + signals.in_access.send(sender=self, event=event) + +# in signals.py +import django.dispatch + +in_access = django.dispatch.Signal(providing_args=["event"]) + +``` + +For our purposes, since we already have an asyncronous celery queue, what we really want is to write to the log of the watcher, and fire off a celery job to add the dicom folder to the database, but only if it's finished. As a reminder, "finished" means it is a directly that does NOT have an extension starting with `.tmp`. For this, we use the `DicomCelery` class under `event_processors` that fires off the `import_dicomdir` async celery task under [main/tasks.py](../sendit/apps/main/tasks.py) instead. + For better understanding, you can look at the code itself, for each of [watcher_start.py](../sendit/apps/watcher/management/commands/watcher_start.py) and [watcher_stop.py](../sendit/apps/watcher/management/commands/watcher_stop.py). + ## Basic Workflow The basic workflow of the watcher is the following: diff --git a/sendit/apps/watcher/__init__.py b/sendit/apps/watcher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendit/apps/watcher/event_processors.py b/sendit/apps/watcher/event_processors.py new file mode 100644 index 0000000..e57d948 --- /dev/null +++ b/sendit/apps/watcher/event_processors.py @@ -0,0 +1,276 @@ +''' +Based on https://github.com/jwineinger/django-inotifier, added Celery usage + +Copyright (c) 2017 Vanessa Sochat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +''' + +import pyinotify +from sendit.logger import bot +from sendit.apps.watcher import signals +from sendit.apps.main.tasks import import_dicomdir +import os + +class DicomCelery(pyinotify.ProcessEvent): + '''class which submits a celery job when a dicom directory is finished + creation. This is determined based on not having any tmp extension + ''' + def is_finished(self,path): + extsep = os.path.extsep + if os.path.isdir(path): + ext = os.path.splitext(path)[1].strip(extsep) + if ext.startswith('tmp'): + return False + return True + else: + return False + + def process_IN_CREATE(self, event): + '''Create should be called when the path is created (or modified) + NOTE: if this isn't the case, use modify instead. + ''' + if self.is_finished(event.pathname): + bot.log("FINISHED: %s" %(event.pathname)) + + # Here is the celery task to use + import_dicomdir.apply_async(kwargs={"dicom_dir":event.pathname}) + else: + bot.log("CREATED: %s" %(event.pathname)) + + + +class AllEventsPrinter(pyinotify.ProcessEvent): + '''A class to print every event. + If logging is true, uses application logger + ''' + def print_logger(self,message): + bot.info(message) + + def println(self,message,logger=False): + if logger is False: + print(message) + else: + self.print_logger(message) + + def process_IN_ACCESS(self, event, logger=False): + message = "IN ACCESS: %s" % event.pathname + self.println(message,logger) + + def process_IN_ATTRIB(self, event, logger=False): + message = "IN ATTRIB: %s" % event.pathname + + def process_IN_CLOSE_NOWRITE(self, event): + message = "IN CLOSE NOWRITE: %s" % event.pathname + self.println(message,logger) + + def process_IN_CLOSE_WRITE(self, event, logger=False): + message = "IN CLOSE WRITE: %s" % event.pathname + self.println(message,logger) + + def process_IN_CREATE(self, event, logger=False): + message = "IN CREATE: %s" % event.pathname + self.println(message,logger) + + def process_IN_DELETE(self, event, logger=False): + message = "IN DELETE: %s" % event.pathname + self.println(message,logger) + + def process_IN_DELETE_SELF(self, event, logger=False): + message = "IN DELETE SELF: %s" % event.pathname + self.println(message,logger) + + def process_IN_IGNORED(self, event, logger=False): + message = "IN IGNORED: %s" % event.pathname + self.println(message,logger) + + def process_IN_MODIFY(self, event, logger=False): + message = "IN MODIFY: %s" % event.pathname + self.println(message,logger) + + def process_IN_MOVE_SELF(self, event, logger=False): + self.println(message,logger) + message = "IN MOVE SELF: %s" % event.pathname + + def process_IN_MOVED_FROM(self, event, logger=False): + message = "IN MOVED FROM: %s" % event.pathname + self.println(message,logger) + + def process_IN_MOVED_TO(self, event, logger=False): + message = "IN MOVED TO: %s" % event.pathname + self.println(message,logger) + + def process_IN_OPEN(self, event, logger=False): + message = "IN OPEN: %s" % event.pathname + self.println(message,logger) + + def process_IN_Q_OVERFLOW(self, event, logger=False): + message = "IN Q OVERFLOW: %s" % event.pathname + self.println(message,logger) + + def process_IN_UNMOUNT(self, event, logger=False): + message = "IN UNMOUNT: %s" % event.pathname + self.println(message,logger) + +# Below is from https://github.com/jwineinger/django-inotifier, added Celery usage + +class AllEventsSignaler(pyinotify.ProcessEvent): + '''Example class to signal on every event. + ''' + def process_IN_ACCESS(self, event): + signals.in_access.send(sender=self, event=event) + + def process_IN_ATTRIB(self, event): + signals.in_attrib.send(sender=self, event=event) + + def process_IN_CLOSE_NOWRITE(self, event): + signals.in_close_nowrite.send(sender=self, event=event) + + def process_IN_CLOSE_WRITE(self, event): + signals.in_close_write.send(sender=self, event=event) + + def process_IN_CREATE(self, event): + signals.in_create.send(sender=self, event=event) + + def process_IN_DELETE(self, event): + signals.in_delete.send(sender=self, event=event) + + def process_IN_DELETE_SELF(self, event): + signals.in_delete_self.send(sender=self, event=event) + + def process_IN_IGNORED(self, event): + signals.in_ignored.send(sender=self, event=event) + + def process_IN_MODIFY(self, event): + signals.in_modify.send(sender=self, event=event) + + def process_IN_MOVE_SELF(self, event): + signals.in_move_self.send(sender=self, event=event) + + def process_IN_MOVED_FROM(self, event): + signals.in_moved_from.send(sender=self, event=event) + + def process_IN_MOVED_TO(self, event): + signals.in_moved_to.send(sender=self, event=event) + + def process_IN_OPEN(self, event): + signals.in_open.send(sender=self, event=event) + + def process_IN_Q_OVERFLOW(self, event): + signals.in_q_overflow.send(sender=self, event=event) + + def process_IN_UNMOUNT(self, event): + signals.in_unmount.send(sender=self, event=event) + + +class CreateSignaler(pyinotify.ProcessEvent): + '''Example class which only signals on create events. + ''' + def process_IN_CREATE(self, event): + signals.in_create.send(sender=self, event=event) + + + + +class CreateViaChunksSignaler(pyinotify.ProcessEvent): + """ + A class which will signal the in_create event after a specific set of + events has occurred for a file. A file transfer agent may write partial + chunks of a file to disk, and then move/rename that partial file to the + final filename once all chunks are written. For this use, we cannot watch + only the IN_CREATE event. Observe the following event history: + + IN CREATE: /path/to/file/filename.part + IN OPEN: /path/to/file/filename.part + IN MODIFY: /path/to/file/filename.part + IN MODIFY: /path/to/file/filename.part + ... + IN MODIFY: /path/to/file/filename.part + IN CLOSE WRITE: /path/to/file/filename.part + IN MOVED FROM: /path/to/file/filename.part + IN MOVED TO: /path/to/file/filename + + Since we only want to signal when a new file is created, not just when + a file is moved/renamed, we will use a state-machine to watch the stream + of events, and only signal when the proper flow has been observed. + Watching only the IN_MOVED_TO event would be insufficient because that + could be triggered by someone manually renaming a file in the directory. + + To accomplish what we want, we will watch the IN_CREATE, IN_MOVED_FROM, + and IN_MOVED_TO events. In the special case of moving a file when both the + source and destination directories are being watched, the IN_MOVED_TO event + will also have a src_pathname attribute to tell what the original filename + was. Since we are only concerned with files created and moved within our + watched directories, this allows us to track the state from the creation + of the temporary .part file all the way through moving it to the final + filename. + """ + def my_init(self): + """ + Setup a dict we can track .part files in. + + Also define the max_allowable_age for an entry in the tracking dict. + """ + self.temp_files = {} + + from datetime import timedelta + self.max_allowed_age = timedelta(hours=6) + + def sleepshop(self): + """ + Remove values from the temp_files instance variable when they + have reached an unacceptable age. + """ + from datetime import datetime + now = datetime.now() + for filename, timestamp in dict(self.temp_files).iteritems(): + if now - timestamp > self.max_allowed_age: + del self.temp_files[filename] + + def process_IN_CREATE(self, event): + """ + The IN_CREATE event will be the creation of the .part temporary file. + + So instead of sending the in_create signal now, we merely start to + track the state of this temporary file. We store a current timestamp + in the tracking dict so we can cull any values which have been around + too long. + """ + self.sleepshop() + + if event.pathname.endswith('.part'): + import datetime + self.temp_files[event.pathname] = datetime.datetime.now() + + def process_IN_MOVED_TO(self, event): + """ + If both the source and destination directories are being watched by + pyinotify, then this event will have a value in src_pathname. This + value will be the .part filename and event.pathname will be the final + filename. + + Note: IN_MOVED_FROM must be watched as well for src_pathname to be + set. + """ + if event.src_pathname and event.src_pathname in self.temp_files: + del self.temp_files[event.src_pathname] + signals.in_create.send(sender=self, event=event) + + self.sleepshop() diff --git a/sendit/apps/watcher/management/__init__.py b/sendit/apps/watcher/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendit/apps/watcher/management/commands/__init__.py b/sendit/apps/watcher/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sendit/apps/watcher/management/commands/watcher_start.py b/sendit/apps/watcher/management/commands/watcher_start.py new file mode 100644 index 0000000..a51c01c --- /dev/null +++ b/sendit/apps/watcher/management/commands/watcher_start.py @@ -0,0 +1,98 @@ +from sendit.logger import bot +from django.core.management.base import ( + BaseCommand, + CommandError +) + +class Command(BaseCommand): + help = '''Starts monitoring the instance /data folder for file events, + specifically for the addition of complete DICOM series datasets''' + + def get_level(self): + import six + if six.PY3: + return 0 + else: + return -1 + + + def handle(self, *args, **options): + from django.conf import settings + import os.path + + # Verify INOTIFIER_WATCH_PATHS is defined and non-empty + try: + assert settings.INOTIFIER_WATCH_PATHS + except (AttributeError, AssertionError): + raise CommandError('Missing/empty settings/watcher.py INOTIFIER_WATCH_PATHS') + + # Verify INOTIFIER_WATCH_PATHS is properly formatted + try: + length_3 = [len(tup) == 3 for tup in settings.INOTIFIER_WATCH_PATHS] + assert all(length_3) + except AssertionError: + msg = '''setting INOTIFIER_WATCH_PATHS should be an iterable of + 3-tuples of the form + [ ("/path1/", , ), ]''' + raise CommandError(msg) + + # We need to give the import function a level based on python version + level = self.get_level() + + # Verify monitor_paths exists and processor classes can be imported + for monitor, m, processor_cls in settings.INOTIFIER_WATCH_PATHS: + if not os.path.exists(monitor): + err = "%s does not exist or you have insufficient permission" % monitor + raise CommandError(err) + path = '.'.join(processor_cls.split('.')[0:-1]) + cls = processor_cls.split('.')[-1] + try: + mod = __import__(path, globals(), locals(), [cls], level) + getattr(mod, cls) + except ImportError as e: + err = 'Cannot import event processor module: %s\n\n%s' \ + % (path, e) + raise CommandError(err) + except AttributeError: + raise CommandError("%s does not exist in %s" % (cls, path)) + + # Verify pyinotify is installed + try: + import pyinotify + except ImportError as e: + raise CommandError("Cannot import pyinotify: %s" % e) + + # Setup watches using pyinotify + wm = pyinotify.WatchManager() + for path, mask, processor_cls in settings.INOTIFIER_WATCH_PATHS: + cls_path = '.'.join(processor_cls.split('.')[0:-1]) + cls = processor_cls.split('.')[-1] + + mod = __import__(cls_path, globals(), locals(), [cls], level) + Processor = getattr(mod, cls) + wm.add_watch(path, mask, proc_fun=Processor()) + bot.debug("Adding watch on %s, processed by %s" %(path, processor_cls)) + + notifier = pyinotify.Notifier(wm) + + # Setup pid file location. Try to use PROJECT_PATH but default to /tmp + try: + pid_file = os.path.join(settings.BASE_DIR, 'watcher.pid') + except AttributeError: + pid_file = os.path.join("/tmp", "watcher.pid") + + # Daemonize, killing any existing process specified in pid file + daemon_kwargs = {} + try: + daemon_kwargs['stdout'] = settings.INOTIFIER_DAEMON_STDOUT + except AttirbuteError: + pass + + try: + daemon_kwargs['stderr'] = settings.INOTIFIER_DAEMON_STDERR + except AttirbuteError: + pass + + notifier.loop(daemonize=True, pid_file=pid_file, **daemon_kwargs) + + bot.debug("Dicom monitoring started") diff --git a/sendit/apps/watcher/management/commands/watcher_stop.py b/sendit/apps/watcher/management/commands/watcher_stop.py new file mode 100644 index 0000000..e6b498d --- /dev/null +++ b/sendit/apps/watcher/management/commands/watcher_stop.py @@ -0,0 +1,41 @@ +from sendit.logger import bot +from django.core.management.base import ( + BaseCommand, + CommandError +) + +class Command(BaseCommand): + help = 'Stops monitoring the /data directory' + + def handle(self, *args, **options): + from django.conf import settings + import os.path + + try: + pid_file = os.path.join(settings.BASE_DIR, 'watcher.pid') + except AttributeError: + pid_file = os.path.join("/tmp", "watcher.pid") + + if os.path.exists(pid_file): + pid = int(open(pid_file).read()) + + import signal + try: + os.kill(pid, signal.SIGHUP) + except OSError: + os.remove(pid_file) + err = "No process with id %d. Removed %s." % (pid, pid_file) + raise CommandError(err) + + import time + time.sleep(2) + + try: + os.kill(pid, signal.SIGKILL) + except OSError: + pass + + os.remove(pid_file) + bot.debug("Dicom watching has been stopped.") + else: + raise CommandError("No pid file exists at %s." % pid_file) diff --git a/sendit/apps/watcher/signals.py b/sendit/apps/watcher/signals.py new file mode 100644 index 0000000..3d6ed33 --- /dev/null +++ b/sendit/apps/watcher/signals.py @@ -0,0 +1,38 @@ +# https://docs.djangoproject.com/en/1.11/topics/signals/#defining-signals + +from django.core.signals import request_finished +from django.dispatch import receiver +import django.dispatch + +in_access = django.dispatch.Signal(providing_args=["event"]) +in_attrib = django.dispatch.Signal(providing_args=["event"]) +in_close_nowrite = django.dispatch.Signal(providing_args=["event"]) +in_close_write = django.dispatch.Signal(providing_args=["event"]) +in_create = django.dispatch.Signal(providing_args=["event"]) +in_delete = django.dispatch.Signal(providing_args=["event"]) +in_delete_self = django.dispatch.Signal(providing_args=["event"]) +in_ignored = django.dispatch.Signal(providing_args=["event"]) +in_modify = django.dispatch.Signal(providing_args=["event"]) +in_move_self = django.dispatch.Signal(providing_args=["event"]) +in_moved_from = django.dispatch.Signal(providing_args=["event"]) +in_moved_to = django.dispatch.Signal(providing_args=["event"]) +in_open = django.dispatch.Signal(providing_args=["event"]) +in_q_overflow = django.dispatch.Signal(providing_args=["event"]) +in_unmount = django.dispatch.Signal(providing_args=["event"]) + +# Example Callbacks + +@receiver(request_finished) +def in_access_callback(in_access, **kwargs): + print("in_access finished!") + print(event) + +@receiver(request_finished) +def in_attrib_callback(in_attrib, **kwargs): + print("in_attrib finished!") + print(event) + +@receiver(request_finished) +def in_open_callback(in_open, **kwargs): + print("in_open finished!") + print(event) diff --git a/sendit/settings/watcher.py b/sendit/settings/watcher.py index df9eb3b..1254322 100644 --- a/sendit/settings/watcher.py +++ b/sendit/settings/watcher.py @@ -8,9 +8,9 @@ def generate_watch_paths(): ''' import pyinotify return ( - ( os.path.join(BASE_DIR,'data'), - pyinotify.ALL_EVENTS, - 'inotifier.event_processors.AllEventsSignaler', + ( os.path.join('/data'), + pyinotify.ALL_EVENTS, + 'sendit.apps.watcher.event_processors.DicomCelery', ), )