diff --git a/geomagio/Url.py b/geomagio/Url.py
new file mode 100644
index 0000000000000000000000000000000000000000..636a2dd656d93250d76918e68775377ce50c425e
--- /dev/null
+++ b/geomagio/Url.py
@@ -0,0 +1,239 @@
+"""Class to read a file from a URL given a template"""
+import os
+import urllib2
+from TimeseriesFactoryException import TimeseriesFactoryException
+
+
+class URL():
+    """URL class to allow reading of files using the urllib2 class
+
+    Parameters
+    ----------
+    urlTemplate : str
+        A string that contains any of the following replacement patterns:
+        - '%(i)s' : interval abbreviation
+        - '%(interval)s' interval name
+        - '%(julian)s' julian date
+        - '%(obs)s' lowercase observatory code
+        - '%(OBS)s' uppercase observatory code
+        - '%(t)s' type abbreviation
+        - '%(type)s' type name
+        - '%(year)s' year formatted as YYYY
+        - '%(ymd)s' time formatted as YYYYMMDD
+    """
+
+    def __init__(self, urlTemplate):
+        self.urlTemplate = urlTemplate
+
+    def get_file_from_url(self, url):
+        """Get a file for writing.
+
+        Ensures parent directory exists.
+
+        Parameters
+        ----------
+        url : str
+            path to file
+
+        Returns
+        -------
+        str
+            path to file without file:// prefix
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if url does not start with file://
+        """
+        if not url.startswith('file://'):
+            raise TimeseriesFactoryException(
+                    'Only file urls are supported for writing')
+        filename = url.replace('file://', '')
+        parent = os.path.dirname(filename)
+        if not os.path.exists(parent):
+            os.makedirs(parent)
+        return filename
+
+    def get_url(self, observatory, date, type='variation', interval='minute'):
+        """Get the url for a specified file.
+
+        Replaces patterns (described in class docstring) with values based on
+        parameter values.
+
+        Parameters
+        ----------
+        observatory : str
+            observatory code.
+        date : obspy.core.UTCDateTime
+            day to fetch (only year, month, day are used)
+        type : {'variation', 'quasi-definitive', 'definitive'}
+            data type.
+        interval : {'minute', 'second', 'hourly', 'daily'}
+            data interval.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if type or interval are not supported.
+        """
+        return self.urlTemplate % {
+                'i': self._get_interval_abbreviation(interval),
+                'interval': self._get_interval_name(interval),
+                'julian': date.strftime("%j"),
+                'obs': observatory.lower(),
+                'OBS': observatory.upper(),
+                't': self._get_type_abbreviation(type),
+                'type': self._get_type_name(type),
+                'year': date.strftime("%Y"),
+                'ymd': date.strftime('%Y%m%d')}
+
+    def read_url(self, url):
+        """Open and read url contents.
+
+        Parameters
+        ----------
+        url : str
+            A urllib2 compatible url, such as http:// or file://.
+
+        Returns
+        -------
+        str
+            contents returned by url.
+
+        Raises
+        ------
+        urllib2.URLError
+            if any occurs
+        """
+        response = urllib2.urlopen(url)
+        content = None
+        try:
+            content = response.read()
+        except urllib2.URLError, e:
+            print e.reason
+            raise
+        finally:
+            response.close()
+        return content
+
+    def _get_interval_abbreviation(self, interval):
+        """Get abbreviation for a data interval.
+
+        Used by ``_get_url`` to replace ``%(i)s`` in urlTemplate.
+
+        Parameters
+        ----------
+        interval : {'daily', 'hourly', 'minute', 'monthly', 'second'}
+
+        Returns
+        -------
+        abbreviation for ``interval``.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if ``interval`` is not supported.
+        """
+        interval_abbr = None
+        if interval == 'daily':
+            interval_abbr = 'day'
+        elif interval == 'hourly':
+            interval_abbr = 'hor'
+        elif interval == 'minute':
+            interval_abbr = 'min'
+        elif interval == 'monthly':
+            interval_abbr = 'mon'
+        elif interval == 'second':
+            interval_abbr = 'sec'
+        else:
+            raise TimeseriesFactoryException(
+                    'Unexpected interval "%s"' % interval)
+        return interval_abbr
+
+    def _get_interval_name(self, interval):
+        """Get name for a data interval.
+
+        Used by ``_get_url`` to replace ``%(interval)s`` in urlTemplate.
+
+        Parameters
+        ----------
+        interval : {'minute', 'second'}
+
+        Returns
+        -------
+        name for ``interval``.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if ``interval`` is not supported.
+        """
+        interval_name = None
+        if interval == 'minute':
+            interval_name = 'OneMinute'
+        elif interval == 'second':
+            interval_name = 'OneSecond'
+        else:
+            raise TimeseriesFactoryException(
+                    'Unsupported interval "%s"' % interval)
+        return interval_name
+
+    def _get_type_abbreviation(self, type):
+        """Get abbreviation for a data type.
+
+        Used by ``_get_url`` to replace ``%(t)s`` in urlTemplate.
+
+        Parameters
+        ----------
+        type : {'definitive', 'provisional', 'quasi-definitive', 'variation'}
+
+        Returns
+        -------
+        name for ``type``.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if ``type`` is not supported.
+        """
+        type_abbr = None
+        if type == 'definitive':
+            type_abbr = 'd'
+        elif type == 'provisional':
+            type_abbr = 'p'
+        elif type == 'quasi-definitive':
+            type_abbr = 'q'
+        elif type == 'variation':
+            type_abbr = 'v'
+        else:
+            raise TimeseriesFactoryException(
+                    'Unexpected type "%s"' % type)
+        return type_abbr
+
+    def _get_type_name(self, type):
+        """Get name for a data type.
+
+        Used by ``_get_url`` to replace ``%(type)s`` in urlTemplate.
+
+        Parameters
+        ----------
+        type : {'variation', 'quasi-definitive'}
+
+        Returns
+        -------
+        name for ``type``.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if ``type`` is not supported.
+        """
+        type_name = None
+        if type == 'variation':
+            type_name = ''
+        elif type == 'quasi-definitive':
+            type_name = 'QuasiDefinitive'
+        else:
+            raise TimeseriesFactoryException(
+                    'Unsupported type "%s"' % type)
+        return type_name
diff --git a/geomagio/__init__.py b/geomagio/__init__.py
index 74c7d9ea771a19bcb02cd7068c234e885425d0f8..47c82746e0de2803d10b49eb2c8907202e162587 100644
--- a/geomagio/__init__.py
+++ b/geomagio/__init__.py
@@ -11,6 +11,7 @@ from TimeseriesFactory import TimeseriesFactory
 from TimeseriesFactoryException import TimeseriesFactoryException
 import TimeseriesUtility
 import Util
+import Url
 from XYZAlgorithm import XYZAlgorithm
 
 __all__ = [
@@ -24,5 +25,6 @@ __all__ = [
     'TimeseriesFactoryException',
     'TimeseriesUtility',
     'Util',
+    'Url',
     'XYZAlgorithm'
 ]
diff --git a/geomagio/iaga2002/IAGA2002Factory.py b/geomagio/iaga2002/IAGA2002Factory.py
index 1017cc4e21a258beae7539248c6ad6ea047f72dc..43c94f7679b5c4af4df850ca730706bc9bd50719 100644
--- a/geomagio/iaga2002/IAGA2002Factory.py
+++ b/geomagio/iaga2002/IAGA2002Factory.py
@@ -1,11 +1,10 @@
 """Factory that loads IAGA2002 Files."""
 
 import obspy.core
-import os
-import urllib2
 from .. import ChannelConverter
 from ..TimeseriesFactory import TimeseriesFactory
 from ..TimeseriesFactoryException import TimeseriesFactoryException
+from ..Url import URL
 from IAGA2002Parser import IAGA2002Parser
 from IAGA2002Writer import IAGA2002Writer
 
@@ -14,36 +13,6 @@ from IAGA2002Writer import IAGA2002Writer
 IAGA_FILE_PATTERN = '%(obs)s%(ymd)s%(t)s%(i)s.%(i)s'
 
 
-def read_url(url):
-    """Open and read url contents.
-
-    Parameters
-    ----------
-    url : str
-        A urllib2 compatible url, such as http:// or file://.
-
-    Returns
-    -------
-    str
-        contents returned by url.
-
-    Raises
-    ------
-    urllib2.URLError
-        if any occurs
-    """
-    response = urllib2.urlopen(url)
-    content = None
-    try:
-        content = response.read()
-    except urllib2.URLError, e:
-        print e.reason
-        raise
-    finally:
-        response.close()
-    return content
-
-
 class IAGA2002Factory(TimeseriesFactory):
     """TimeseriesFactory for IAGA 2002 formatted files.
 
@@ -103,9 +72,10 @@ class IAGA2002Factory(TimeseriesFactory):
         interval = interval or self.interval
         days = self._get_days(starttime, endtime)
         timeseries = obspy.core.Stream()
+        url = URL(self.urlTemplate)
         for day in days:
-            url = self._get_url(observatory, day, type, interval)
-            iagaFile = read_url(url)
+            url_id = url.get_url(observatory, day, type, interval)
+            iagaFile = url.read_url(url_id)
             timeseries += self.parse_string(iagaFile)
         # merge channel traces for multiple days
         timeseries.merge()
@@ -147,159 +117,6 @@ class IAGA2002Factory(TimeseriesFactory):
             stream += obspy.core.Trace(data[channel], stats)
         return stream
 
-    def _get_url(self, observatory, date, type='variation', interval='minute'):
-        """Get the url for a specified IAGA2002 file.
-
-        Replaces patterns (described in class docstring) with values based on
-        parameter values.
-
-        Parameters
-        ----------
-        observatory : str
-            observatory code.
-        date : obspy.core.UTCDateTime
-            day to fetch (only year, month, day are used)
-        type : {'variation', 'quasi-definitive'}
-            data type.
-        interval : {'minute', 'second'}
-            data interval.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if type or interval are not supported.
-        """
-        return self.urlTemplate % {
-                'i': self._get_interval_abbreviation(interval),
-                'interval': self._get_interval_name(interval),
-                'obs': observatory.lower(),
-                'OBS': observatory.upper(),
-                't': self._get_type_abbreviation(type),
-                'type': self._get_type_name(type),
-                'ymd': date.strftime("%Y%m%d")}
-
-    def _get_interval_abbreviation(self, interval):
-        """Get abbreviation for a data interval.
-
-        Used by ``_get_url`` to replace ``%(i)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        interval : {'daily', 'hourly', 'minute', 'monthly', 'second'}
-
-        Returns
-        -------
-        abbreviation for ``interval``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``interval`` is not supported.
-        """
-        interval_abbr = None
-        if interval == 'daily':
-            interval_abbr = 'day'
-        elif interval == 'hourly':
-            interval_abbr = 'hor'
-        elif interval == 'minute':
-            interval_abbr = 'min'
-        elif interval == 'monthly':
-            interval_abbr = 'mon'
-        elif interval == 'second':
-            interval_abbr = 'sec'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unexpected interval "%s"' % interval)
-        return interval_abbr
-
-    def _get_interval_name(self, interval):
-        """Get name for a data interval.
-
-        Used by ``_get_url`` to replace ``%(interval)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        interval : {'minute', 'second'}
-
-        Returns
-        -------
-        name for ``interval``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``interval`` is not supported.
-        """
-        interval_name = None
-        if interval == 'minute':
-            interval_name = 'OneMinute'
-        elif interval == 'second':
-            interval_name = 'OneSecond'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unsupported interval "%s"' % interval)
-        return interval_name
-
-    def _get_type_abbreviation(self, type):
-        """Get abbreviation for a data type.
-
-        Used by ``_get_url`` to replace ``%(t)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        type : {'definitive', 'provisional', 'quasi-definitive', 'variation'}
-
-        Returns
-        -------
-        name for ``type``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``type`` is not supported.
-        """
-        type_abbr = None
-        if type == 'definitive':
-            type_abbr = 'd'
-        elif type == 'provisional':
-            type_abbr = 'p'
-        elif type == 'quasi-definitive':
-            type_abbr = 'q'
-        elif type == 'variation':
-            type_abbr = 'v'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unexpected type "%s"' % type)
-        return type_abbr
-
-    def _get_type_name(self, type):
-        """Get name for a data type.
-
-        Used by ``_get_url`` to replace ``%(type)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        type : {'variation', 'quasi-definitive'}
-
-        Returns
-        -------
-        name for ``type``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``type`` is not supported.
-        """
-        type_name = None
-        if type == 'variation':
-            type_name = ''
-        elif type == 'quasi-definitive':
-            type_name = 'QuasiDefinitive'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unsupported type "%s"' % type)
-        return type_name
-
     def _get_days(self, starttime, endtime):
         """Get days between (inclusive) starttime and endtime.
 
@@ -382,42 +199,14 @@ class IAGA2002Factory(TimeseriesFactory):
         starttime = starttime or stats.starttime
         endtime = endtime or stats.endtime
         days = self._get_days(starttime, endtime)
+        url = URL(self.urlTemplate)
         for day in days:
-            day_filename = self._get_file_from_url(
-                    self._get_url(observatory, day, type, interval))
+            day_filename = url.get_file_from_url(
+                    url.get_url(observatory, day, type, interval))
             day_timeseries = self._get_slice(timeseries, day, interval)
             with open(day_filename, 'wb') as fh:
                 self.write_file(fh, day_timeseries, channels)
 
-    def _get_file_from_url(self, url):
-        """Get a file for writing.
-
-        Ensures parent directory exists.
-
-        Parameters
-        ----------
-        url : str
-            Url path to IAGA2002
-
-        Returns
-        -------
-        str
-            path to file without file:// prefix
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if url does not start with file://
-        """
-        if not url.startswith('file://'):
-            raise TimeseriesFactoryException(
-                    'Only file urls are supported for writing')
-        filename = url.replace('file://', '')
-        parent = os.path.dirname(filename)
-        if not os.path.exists(parent):
-            os.makedirs(parent)
-        return filename
-
     def _get_slice(self, timeseries, day, interval):
         """Get the first and last time for a day
 
diff --git a/geomagio/imfv283/IMFV283Factory.py b/geomagio/imfv283/IMFV283Factory.py
index 530ca8e6518a53059db9b7fe7607ab3fb80e412b..2a33a40e7c9fc22f60e40e5985774c16e359f4a0 100644
--- a/geomagio/imfv283/IMFV283Factory.py
+++ b/geomagio/imfv283/IMFV283Factory.py
@@ -1,64 +1,17 @@
 """Factory that loads IAGA2002 Files."""
 
 import obspy.core
-import os
-import urllib2
 import numpy
 from .. import ChannelConverter
 from ..TimeseriesFactory import TimeseriesFactory
 from ..TimeseriesFactoryException import TimeseriesFactoryException
+from ..Url import URL
 from IMFV283Parser import IMFV283Parser
 
 
-# pattern for IMFV283 file names
-IMFV283_FILE_PATTERN = 'dcpmsgs.txt'
-
-
-def read_url(url):
-    """Open and read url contents.
-
-    Parameters
-    ----------
-    url : str
-        A urllib2 compatible url, such as http:// or file://.
-
-    Returns
-    -------
-    str
-        contents returned by url.
-
-    Raises
-    ------
-    urllib2.URLError
-        if any occurs
-    """
-    response = urllib2.urlopen(url)
-    content = None
-    try:
-        content = response.read()
-    except urllib2.URLError, e:
-        print e.reason
-        raise
-    finally:
-        response.close()
-    return content
-
-
 class IMFV283Factory(TimeseriesFactory):
     """TimeseriesFactory for IMFV283 formatted files.
 
-    Parameters
-    ----------
-    urlTemplate : str
-        A string that contains any of the following replacement patterns:
-        - '%(i)s' : interval abbreviation
-        - '%(interval)s' interval name
-        - '%(obs)s' lowercase observatory code
-        - '%(OBS)s' uppercase observatory code
-        - '%(t)s' type abbreviation
-        - '%(type)s' type name
-        - '%(ymd)s' time formatted as YYYYMMDD
-
     See Also
     --------
     IMFV283Parser
@@ -108,10 +61,11 @@ class IMFV283Factory(TimeseriesFactory):
         type = type or self.type
         interval = interval or self.interval
         timeseries = obspy.core.Stream()
-
-        url = self._get_url(observatory, obspy.core.UTCDateTime(),
+        url = URL(self.urlTemplate)
+        url_id = url.get_url(observatory, obspy.core.UTCDateTime(),
                 type, interval)
-        imfV283File = read_url(url)
+
+        imfV283File = url.read_url(url_id)
         timeseries += self.parse_string(imfV283File)
         # merge channel traces for multiple days
         timeseries.merge()
@@ -151,195 +105,6 @@ class IMFV283Factory(TimeseriesFactory):
 
         return stream
 
-    def _get_url(self, observatory, date, type='variation', interval='minute'):
-        """Get the url for a specified IMFV283 file.
-
-        Replaces patterns (described in class docstring) with values based on
-        parameter values.
-
-        Parameters
-        ----------
-        observatory : str
-            observatory code.
-        date : obspy.core.UTCDateTime
-            day to fetch (only year, month, day are used)
-        type : {'variation', 'quasi-definitive'}
-            data type.
-        interval : {'minute', 'second'}
-            data interval.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if type or interval are not supported.
-        """
-        return self.urlTemplate % {
-                'i': self._get_interval_abbreviation(interval),
-                'interval': self._get_interval_name(interval),
-                'obs': observatory.lower(),
-                'OBS': observatory.upper(),
-                't': self._get_type_abbreviation(type),
-                'type': self._get_type_name(type),
-                'ymd': date.strftime("%Y%m%d")}
-
-    def _get_interval_abbreviation(self, interval):
-        """Get abbreviation for a data interval.
-
-        Used by ``_get_url`` to replace ``%(i)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        interval : {'daily', 'hourly', 'minute', 'monthly', 'second'}
-
-        Returns
-        -------
-        abbreviation for ``interval``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``interval`` is not supported.
-        """
-        print interval
-        interval_abbr = None
-        if interval == 'daily':
-            interval_abbr = 'day'
-        elif interval == 'hourly':
-            interval_abbr = 'hor'
-        elif interval == 'minute':
-            interval_abbr = 'min'
-        elif interval == 'monthly':
-            interval_abbr = 'mon'
-        elif interval == 'second':
-            interval_abbr = 'sec'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unexpected interval "%s"' % interval)
-        return interval_abbr
-
-    def _get_interval_name(self, interval):
-        """Get name for a data interval.
-
-        Used by ``_get_url`` to replace ``%(interval)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        interval : {'minute', 'second'}
-
-        Returns
-        -------
-        name for ``interval``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``interval`` is not supported.
-        """
-        interval_name = None
-        if interval == 'minute':
-            interval_name = 'OneMinute'
-        elif interval == 'second':
-            interval_name = 'OneSecond'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unsupported interval "%s"' % interval)
-        return interval_name
-
-    def _get_type_abbreviation(self, type):
-        """Get abbreviation for a data type.
-
-        Used by ``_get_url`` to replace ``%(t)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        type : {'definitive', 'provisional', 'quasi-definitive', 'variation'}
-
-        Returns
-        -------
-        name for ``type``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``type`` is not supported.
-        """
-        type_abbr = None
-        if type == 'definitive':
-            type_abbr = 'd'
-        elif type == 'provisional':
-            type_abbr = 'p'
-        elif type == 'quasi-definitive':
-            type_abbr = 'q'
-        elif type == 'variation':
-            type_abbr = 'v'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unexpected type "%s"' % type)
-        return type_abbr
-
-    def _get_type_name(self, type):
-        """Get name for a data type.
-
-        Used by ``_get_url`` to replace ``%(type)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        type : {'variation', 'quasi-definitive'}
-
-        Returns
-        -------
-        name for ``type``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``type`` is not supported.
-        """
-        type_name = None
-        if type == 'variation':
-            type_name = ''
-        elif type == 'quasi-definitive':
-            type_name = 'QuasiDefinitive'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unsupported type "%s"' % type)
-        return type_name
-
-    def _get_days(self, starttime, endtime):
-        """Get days between (inclusive) starttime and endtime.
-
-        Parameters
-        ----------
-        starttime : obspy.core.UTCDateTime
-            the start time
-        endtime : obspy.core.UTCDateTime
-            the end time
-
-        Returns
-        -------
-        array_like
-            list of times, one per day, for all days between and including
-            ``starttime`` and ``endtime``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if starttime is after endtime
-        """
-        if starttime > endtime:
-            raise TimeseriesFactoryException(
-                    'starttime must be before endtime')
-        days = []
-        day = starttime
-        lastday = (endtime.year, endtime.month, endtime.day)
-        while True:
-            days.append(day)
-            if lastday == (day.year, day.month, day.day):
-                break
-            # move to next day
-            day = obspy.core.UTCDateTime(day.timestamp + 86400)
-        return days
-
     def write_file(self, fh, timeseries, channels):
         """writes timeseries data to the given file object.
 
@@ -352,32 +117,3 @@ class IMFV283Factory(TimeseriesFactory):
             list of channels to store
         """
         raise TimeseriesFactoryException('IAF write_file not implemented.')
-
-    def _get_file_from_url(self, url):
-        """Get a file for writing.
-
-        Ensures parent directory exists.
-
-        Parameters
-        ----------
-        url : str
-            Url path to IMFV283
-
-        Returns
-        -------
-        str
-            path to file without file:// prefix
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if url does not start with file://
-        """
-        if not url.startswith('file://'):
-            raise TimeseriesFactoryException(
-                    'Only file urls are supported for writing')
-        filename = url.replace('file://', '')
-        parent = os.path.dirname(filename)
-        if not os.path.exists(parent):
-            os.makedirs(parent)
-        return filename
diff --git a/geomagio/pcdcp/PCDCPFactory.py b/geomagio/pcdcp/PCDCPFactory.py
index 8d6c0e82be2686887847574ed2b6b90a353c517a..d6b961240904b51b4a893f72fbc576bae6b91ff0 100644
--- a/geomagio/pcdcp/PCDCPFactory.py
+++ b/geomagio/pcdcp/PCDCPFactory.py
@@ -1,11 +1,10 @@
 """Factory that loads PCDCP Files."""
 
 import obspy.core
-import os
-import urllib2
 from .. import ChannelConverter
 from ..TimeseriesFactory import TimeseriesFactory
 from ..TimeseriesFactoryException import TimeseriesFactoryException
+from ..Url import URL
 from PCDCPParser import PCDCPParser
 from PCDCPWriter import PCDCPWriter
 
@@ -14,36 +13,6 @@ from PCDCPWriter import PCDCPWriter
 PCDCP_FILE_PATTERN = '%(obs)s%(y)s%(j)s.%(i)s'
 
 
-def read_url(url):
-    """Open and read url contents.
-
-    Parameters
-    ----------
-    url : str
-        A urllib2 compatible url, such as http:// or file://.
-
-    Returns
-    -------
-    str
-        contents returned by url.
-
-    Raises
-    ------
-    urllib2.URLError
-        if any occurs
-    """
-    response = urllib2.urlopen(url)
-    content = None
-    try:
-        content = response.read()
-    except urllib2.URLError, e:
-        print e.reason
-        raise
-    finally:
-        response.close()
-    return content
-
-
 class PCDCPFactory(TimeseriesFactory):
     """TimeseriesFactory for PCDCP formatted files.
 
@@ -105,10 +74,10 @@ class PCDCPFactory(TimeseriesFactory):
         interval = interval or self.interval
         days = self._get_days(starttime, endtime)
         timeseries = obspy.core.Stream()
-
+        url = URL(self.urlTemplate)
         for day in days:
-            url = self._get_url(observatory, day, type, interval)
-            pcdcpFile = read_url(url)
+            url_id = url.get_url(observatory, day, type, interval)
+            pcdcpFile = url.read_url(url_id)
             timeseries += self.parse_string(pcdcpFile)
 
         # merge channel traces for multiple days
@@ -174,162 +143,6 @@ class PCDCPFactory(TimeseriesFactory):
 
         return stream
 
-    def _get_url(self, observatory, date, type='variation', interval='minute'):
-        """Get the url for a specified PCDCP file.
-
-        Replaces patterns (described in class docstring) with values based on
-        parameter values.
-
-        Parameters
-        ----------
-        observatory : str
-            observatory code.
-        date : obspy.core.UTCDateTime
-            day to fetch (only year, month, day are used)
-        type : {'variation', 'quasi-definitive'}
-            data type.
-        interval : {'minute', 'second'}
-            data interval.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            If type or interval are not supported.
-        """
-        return self.urlTemplate % {
-                                'i': self._get_interval_abbreviation(interval),
-                                'interval': self._get_interval_name(interval),
-                                'julian': date.strftime("%j"),
-                                'obs': observatory.lower(),
-                                'OBS': observatory.upper(),
-                                't': self._get_type_abbreviation(type),
-                                'type': self._get_type_name(type),
-                                'year': date.strftime("%Y"),
-                                'ymd': date.strftime("%Y%m%d")
-                                  }
-
-    def _get_interval_abbreviation(self, interval):
-        """Get abbreviation for a data interval.
-
-        Used by ``_get_url`` to replace ``%(i)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        interval : {'daily', 'hourly', 'minute', 'monthly', 'second'}
-
-        Returns
-        -------
-        abbreviation for ``interval``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``interval`` is not supported.
-        """
-        interval_abbr = None
-        if interval == 'daily':
-            interval_abbr = 'day'
-        elif interval == 'hourly':
-            interval_abbr = 'hor'
-        elif interval == 'minute':
-            interval_abbr = 'min'
-        elif interval == 'monthly':
-            interval_abbr = 'mon'
-        elif interval == 'second':
-            interval_abbr = 'sec'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unexpected interval "%s"' % interval)
-        return interval_abbr
-
-    def _get_interval_name(self, interval):
-        """Get name for a data interval.
-
-        Used by ``_get_url`` to replace ``%(interval)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        interval : {'minute', 'second'}
-
-        Returns
-        -------
-        name for ``interval``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``interval`` is not supported.
-        """
-        interval_name = None
-        if interval == 'minute':
-            interval_name = 'OneMinute'
-        elif interval == 'second':
-            interval_name = 'OneSecond'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unsupported interval "%s"' % interval)
-        return interval_name
-
-    def _get_type_abbreviation(self, type):
-        """Get abbreviation for a data type.
-
-        Used by ``_get_url`` to replace ``%(t)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        type : {'definitive', 'provisional', 'quasi-definitive', 'variation'}
-
-        Returns
-        -------
-        name for ``type``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``type`` is not supported.
-        """
-        type_abbr = None
-        if type == 'definitive':
-            type_abbr = 'd'
-        elif type == 'provisional':
-            type_abbr = 'p'
-        elif type == 'quasi-definitive':
-            type_abbr = 'q'
-        elif type == 'variation':
-            type_abbr = 'v'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unexpected type "%s"' % type)
-        return type_abbr
-
-    def _get_type_name(self, type):
-        """Get name for a data type.
-
-        Used by ``_get_url`` to replace ``%(type)s`` in urlTemplate.
-
-        Parameters
-        ----------
-        type : {'variation', 'quasi-definitive'}
-
-        Returns
-        -------
-        name for ``type``.
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if ``type`` is not supported.
-        """
-        type_name = None
-        if type == 'variation':
-            type_name = ''
-        elif type == 'quasi-definitive':
-            type_name = 'QuasiDefinitive'
-        else:
-            raise TimeseriesFactoryException(
-                    'Unsupported type "%s"' % type)
-        return type_name
-
     def _get_days(self, starttime, endtime):
         """Get days between (inclusive) starttime and endtime.
 
@@ -414,47 +227,15 @@ class PCDCPFactory(TimeseriesFactory):
         starttime = starttime or stats.starttime
         endtime = endtime or stats.endtime
         days = self._get_days(starttime, endtime)
-
+        url = URL(self.urlTemplate)
         for day in days:
-            day_filename = self._get_file_from_url(
-                    self._get_url(observatory, day, type, interval))
+            day_filename = url.get_file_from_url(
+                    url.get_url(observatory, day, type, interval))
 
             day_timeseries = self._get_slice(timeseries, day, interval)
             with open(day_filename, 'wb') as fh:
                 self.write_file(fh, day_timeseries, channels)
 
-    def _get_file_from_url(self, url):
-        """Get a file for writing.
-
-        Ensures parent directory exists.
-
-        Parameters
-        ----------
-        url : str
-            Url path to PCDCP
-
-        Returns
-        -------
-        str
-            path to file without file:// prefix
-
-        Raises
-        ------
-        TimeseriesFactoryException
-            if url does not start with file://
-        """
-        if not url.startswith('file://'):
-            raise TimeseriesFactoryException(
-                    'Only file urls are supported for writing')
-
-        filename = url.replace('file://', '')
-        parent = os.path.dirname(filename)
-
-        if not os.path.exists(parent):
-            os.makedirs(parent)
-
-        return filename
-
     def _get_slice(self, timeseries, day, interval):
         """Get the first and last time for a day