toil 8.2.0__py3-none-any.whl → 9.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. toil/batchSystems/abstractBatchSystem.py +13 -5
  2. toil/batchSystems/abstractGridEngineBatchSystem.py +17 -5
  3. toil/batchSystems/kubernetes.py +13 -2
  4. toil/batchSystems/mesos/batchSystem.py +33 -2
  5. toil/batchSystems/registry.py +15 -118
  6. toil/batchSystems/slurm.py +191 -16
  7. toil/common.py +20 -1
  8. toil/cwl/cwltoil.py +97 -119
  9. toil/cwl/utils.py +103 -3
  10. toil/fileStores/__init__.py +1 -1
  11. toil/fileStores/abstractFileStore.py +5 -2
  12. toil/fileStores/cachingFileStore.py +1 -1
  13. toil/job.py +30 -14
  14. toil/jobStores/abstractJobStore.py +35 -255
  15. toil/jobStores/aws/jobStore.py +864 -1964
  16. toil/jobStores/aws/utils.py +24 -270
  17. toil/jobStores/fileJobStore.py +2 -1
  18. toil/jobStores/googleJobStore.py +32 -13
  19. toil/jobStores/utils.py +0 -327
  20. toil/leader.py +27 -22
  21. toil/lib/accelerators.py +1 -1
  22. toil/lib/aws/config.py +22 -0
  23. toil/lib/aws/s3.py +477 -9
  24. toil/lib/aws/utils.py +22 -33
  25. toil/lib/checksum.py +88 -0
  26. toil/lib/conversions.py +33 -31
  27. toil/lib/directory.py +217 -0
  28. toil/lib/ec2.py +97 -29
  29. toil/lib/exceptions.py +2 -1
  30. toil/lib/expando.py +2 -2
  31. toil/lib/generatedEC2Lists.py +138 -19
  32. toil/lib/io.py +33 -2
  33. toil/lib/memoize.py +21 -7
  34. toil/lib/misc.py +1 -1
  35. toil/lib/pipes.py +385 -0
  36. toil/lib/plugins.py +106 -0
  37. toil/lib/retry.py +1 -1
  38. toil/lib/threading.py +1 -1
  39. toil/lib/url.py +320 -0
  40. toil/lib/web.py +4 -5
  41. toil/options/cwl.py +13 -1
  42. toil/options/runner.py +17 -10
  43. toil/options/wdl.py +12 -1
  44. toil/provisioners/__init__.py +5 -2
  45. toil/provisioners/aws/__init__.py +43 -36
  46. toil/provisioners/aws/awsProvisioner.py +47 -15
  47. toil/provisioners/node.py +60 -12
  48. toil/resource.py +3 -13
  49. toil/server/app.py +12 -6
  50. toil/server/cli/wes_cwl_runner.py +2 -2
  51. toil/server/wes/abstract_backend.py +21 -43
  52. toil/server/wes/toil_backend.py +2 -2
  53. toil/test/__init__.py +16 -18
  54. toil/test/batchSystems/batchSystemTest.py +2 -9
  55. toil/test/batchSystems/batch_system_plugin_test.py +7 -0
  56. toil/test/batchSystems/test_slurm.py +103 -14
  57. toil/test/cwl/cwlTest.py +181 -8
  58. toil/test/cwl/staging_cat.cwl +27 -0
  59. toil/test/cwl/staging_make_file.cwl +25 -0
  60. toil/test/cwl/staging_workflow.cwl +43 -0
  61. toil/test/cwl/zero_default.cwl +61 -0
  62. toil/test/docs/scripts/tutorial_staging.py +17 -8
  63. toil/test/docs/scriptsTest.py +2 -1
  64. toil/test/jobStores/jobStoreTest.py +23 -133
  65. toil/test/lib/aws/test_iam.py +7 -7
  66. toil/test/lib/aws/test_s3.py +30 -33
  67. toil/test/lib/aws/test_utils.py +9 -9
  68. toil/test/lib/test_url.py +69 -0
  69. toil/test/lib/url_plugin_test.py +105 -0
  70. toil/test/provisioners/aws/awsProvisionerTest.py +60 -7
  71. toil/test/provisioners/clusterTest.py +15 -2
  72. toil/test/provisioners/gceProvisionerTest.py +1 -1
  73. toil/test/server/serverTest.py +78 -36
  74. toil/test/src/autoDeploymentTest.py +2 -3
  75. toil/test/src/fileStoreTest.py +89 -87
  76. toil/test/utils/ABCWorkflowDebug/ABC.txt +1 -0
  77. toil/test/utils/ABCWorkflowDebug/debugWorkflow.py +4 -4
  78. toil/test/utils/toilKillTest.py +35 -28
  79. toil/test/wdl/md5sum/md5sum-gs.json +1 -1
  80. toil/test/wdl/md5sum/md5sum.json +1 -1
  81. toil/test/wdl/testfiles/read_file.wdl +18 -0
  82. toil/test/wdl/testfiles/url_to_optional_file.wdl +2 -1
  83. toil/test/wdl/wdltoil_test.py +171 -162
  84. toil/test/wdl/wdltoil_test_kubernetes.py +9 -0
  85. toil/utils/toilDebugFile.py +6 -3
  86. toil/utils/toilSshCluster.py +23 -0
  87. toil/utils/toilStats.py +17 -2
  88. toil/utils/toilUpdateEC2Instances.py +1 -0
  89. toil/version.py +10 -10
  90. toil/wdl/wdltoil.py +1179 -825
  91. toil/worker.py +16 -8
  92. {toil-8.2.0.dist-info → toil-9.1.0.dist-info}/METADATA +32 -32
  93. {toil-8.2.0.dist-info → toil-9.1.0.dist-info}/RECORD +97 -85
  94. {toil-8.2.0.dist-info → toil-9.1.0.dist-info}/WHEEL +1 -1
  95. toil/lib/iterables.py +0 -112
  96. toil/test/docs/scripts/stagingExampleFiles/in.txt +0 -1
  97. {toil-8.2.0.dist-info → toil-9.1.0.dist-info}/entry_points.txt +0 -0
  98. {toil-8.2.0.dist-info → toil-9.1.0.dist-info}/licenses/LICENSE +0 -0
  99. {toil-8.2.0.dist-info → toil-9.1.0.dist-info}/top_level.txt +0 -0
@@ -32,6 +32,7 @@ from typing import (
32
32
  Union,
33
33
  cast,
34
34
  overload,
35
+ Type,
35
36
  )
36
37
  from urllib.error import HTTPError
37
38
  from urllib.parse import ParseResult, urlparse
@@ -52,6 +53,7 @@ from toil.lib.exceptions import UnimplementedURLException
52
53
  from toil.lib.io import WriteWatchingStream
53
54
  from toil.lib.memoize import memoize
54
55
  from toil.lib.retry import ErrorCondition, retry
56
+ from toil.lib.url import URLAccess
55
57
 
56
58
  if TYPE_CHECKING:
57
59
  from toil.job import TemporaryID
@@ -88,7 +90,7 @@ class InvalidImportExportUrlException(Exception):
88
90
  class NoSuchJobException(Exception):
89
91
  """Indicates that the specified job does not exist."""
90
92
 
91
- def __init__(self, jobStoreID: FileID):
93
+ def __init__(self, jobStoreID: Union[FileID, str]):
92
94
  """
93
95
  :param str jobStoreID: the jobStoreID that was mistakenly assumed to exist
94
96
  """
@@ -98,7 +100,7 @@ class NoSuchJobException(Exception):
98
100
  class ConcurrentFileModificationException(Exception):
99
101
  """Indicates that the file was attempted to be modified by multiple processes at once."""
100
102
 
101
- def __init__(self, jobStoreFileID: FileID):
103
+ def __init__(self, jobStoreFileID: Union[FileID, str]):
102
104
  """
103
105
  :param jobStoreFileID: the ID of the file that was modified by multiple workers
104
106
  or processes concurrently
@@ -110,7 +112,7 @@ class NoSuchFileException(Exception):
110
112
  """Indicates that the specified file does not exist."""
111
113
 
112
114
  def __init__(
113
- self, jobStoreFileID: FileID, customName: Optional[str] = None, *extra: Any
115
+ self, jobStoreFileID: Union[FileID, str], customName: Optional[str] = None, *extra: Any
114
116
  ):
115
117
  """
116
118
  :param jobStoreFileID: the ID of the file that was mistakenly assumed to exist
@@ -138,11 +140,12 @@ class NoSuchJobStoreException(LocatorException):
138
140
  def __init__(self, locator: str, prefix: str):
139
141
  """
140
142
  :param str locator: The location of the job store
143
+ :param str prefix: The type of job store
141
144
  """
142
145
  super().__init__(
143
146
  "The job store '%s' does not exist, so there is nothing to restart.",
144
147
  locator,
145
- prefix,
148
+ prefix
146
149
  )
147
150
 
148
151
 
@@ -157,7 +160,7 @@ class JobStoreExistsException(LocatorException):
157
160
  "The job store '%s' already exists. Use --restart to resume the workflow, or remove "
158
161
  "the job store with 'toil clean' to start the workflow from scratch.",
159
162
  locator,
160
- prefix,
163
+ prefix
161
164
  )
162
165
 
163
166
 
@@ -238,7 +241,12 @@ class AbstractJobStore(ABC):
238
241
 
239
242
  @property
240
243
  def config(self) -> Config:
241
- """Return the Toil configuration associated with this job store."""
244
+ """
245
+ Return the Toil configuration associated with this job store.
246
+
247
+ :raises AttributeError: if the config has not yet been assigned (i.e.
248
+ during :meth:`resume`).
249
+ """
242
250
  return self.__config
243
251
 
244
252
  @property
@@ -354,23 +362,6 @@ class AbstractJobStore(ABC):
354
362
  jobStoreClasses.append(jobStoreClass)
355
363
  return jobStoreClasses
356
364
 
357
- @classmethod
358
- def _findJobStoreForUrl(
359
- cls, url: ParseResult, export: bool = False
360
- ) -> "AbstractJobStore":
361
- """
362
- Returns the AbstractJobStore subclass that supports the given URL.
363
-
364
- :param ParseResult url: The given URL
365
-
366
- :param bool export: Determines if the url is supported for exporting
367
-
368
- :rtype: toil.jobStore.AbstractJobStore
369
- """
370
- for implementation in cls._get_job_store_classes():
371
- if implementation._supports_url(url, export):
372
- return implementation
373
- raise UnimplementedURLException(url, "export" if export else "import")
374
365
 
375
366
  # Importing a file with a shared file name returns None, but without one it
376
367
  # returns a file ID. Explain this to MyPy.
@@ -464,7 +455,7 @@ class AbstractJobStore(ABC):
464
455
  # optimizations that circumvent this, the _import_file method should be overridden by
465
456
  # subclasses of AbstractJobStore.
466
457
  parseResult = urlparse(src_uri)
467
- otherCls = self._findJobStoreForUrl(parseResult)
458
+ otherCls = URLAccess._find_url_implementation(parseResult)
468
459
  logger.info("Importing input %s...", src_uri)
469
460
  return self._import_file(
470
461
  otherCls,
@@ -476,7 +467,7 @@ class AbstractJobStore(ABC):
476
467
 
477
468
  def _import_file(
478
469
  self,
479
- otherCls: "AbstractJobStore",
470
+ otherCls: Type["URLAccess"],
480
471
  uri: ParseResult,
481
472
  shared_file_name: Optional[str] = None,
482
473
  hardlink: bool = False,
@@ -490,7 +481,7 @@ class AbstractJobStore(ABC):
490
481
 
491
482
  Raises FileNotFoundError if the file does not exist.
492
483
 
493
- :param AbstractJobStore otherCls: The concrete subclass of AbstractJobStore that supports
484
+ :param URLAccess otherCls: The class of URLAccess that supports
494
485
  reading from the given URL and getting the file size from the URL.
495
486
 
496
487
  :param ParseResult uri: The location of the file to import.
@@ -535,16 +526,16 @@ class AbstractJobStore(ABC):
535
526
  from toil.common import Toil
536
527
  dst_uri = Toil.normalize_uri(dst_uri)
537
528
  parseResult = urlparse(dst_uri)
538
- otherCls = self._findJobStoreForUrl(parseResult, export=True)
529
+ otherCls = URLAccess._find_url_implementation(parseResult, export=True)
539
530
  self._export_file(otherCls, file_id, parseResult)
540
531
 
541
532
  def _export_file(
542
- self, otherCls: "AbstractJobStore", jobStoreFileID: FileID, url: ParseResult
533
+ self, otherCls: Type["URLAccess"], jobStoreFileID: FileID, url: ParseResult
543
534
  ) -> None:
544
535
  """
545
536
  Refer to exportFile docstring for information about this method.
546
537
 
547
- :param AbstractJobStore otherCls: The concrete subclass of AbstractJobStore that supports
538
+ :param URLAccess otherCls: The class of URLAccess that supports
548
539
  exporting to the given URL. Note that the type annotation here is not completely
549
540
  accurate. This is not an instance, it's a class, but there is no way to reflect
550
541
  that in :pep:`484` type hints.
@@ -556,12 +547,12 @@ class AbstractJobStore(ABC):
556
547
  self._default_export_file(otherCls, jobStoreFileID, url)
557
548
 
558
549
  def _default_export_file(
559
- self, otherCls: "AbstractJobStore", jobStoreFileID: FileID, url: ParseResult
550
+ self, otherCls: Type["URLAccess"], jobStoreFileID: FileID, url: ParseResult
560
551
  ) -> None:
561
552
  """
562
553
  Refer to exportFile docstring for information about this method.
563
554
 
564
- :param AbstractJobStore otherCls: The concrete subclass of AbstractJobStore that supports
555
+ :param URLAccess otherCls: The class of URLAccess that supports
565
556
  exporting to the given URL. Note that the type annotation here is not completely
566
557
  accurate. This is not an instance, it's a class, but there is no way to reflect
567
558
  that in :pep:`484` type hints.
@@ -576,217 +567,6 @@ class AbstractJobStore(ABC):
576
567
  executable = jobStoreFileID.executable
577
568
  otherCls._write_to_url(readable, url, executable)
578
569
 
579
- @classmethod
580
- def url_exists(cls, src_uri: str) -> bool:
581
- """
582
- Return True if the file at the given URI exists, and False otherwise.
583
-
584
- May raise an error if file existence cannot be determined.
585
-
586
- :param src_uri: URL that points to a file or object in the storage
587
- mechanism of a supported URL scheme e.g. a blob in an AWS s3 bucket.
588
- """
589
- parseResult = urlparse(src_uri)
590
- otherCls = cls._findJobStoreForUrl(parseResult)
591
- return otherCls._url_exists(parseResult)
592
-
593
- @classmethod
594
- def get_size(cls, src_uri: str) -> Optional[int]:
595
- """
596
- Get the size in bytes of the file at the given URL, or None if it cannot be obtained.
597
-
598
- :param src_uri: URL that points to a file or object in the storage
599
- mechanism of a supported URL scheme e.g. a blob in an AWS s3 bucket.
600
- """
601
- parseResult = urlparse(src_uri)
602
- otherCls = cls._findJobStoreForUrl(parseResult)
603
- return otherCls._get_size(parseResult)
604
-
605
- @classmethod
606
- def get_is_directory(cls, src_uri: str) -> bool:
607
- """
608
- Return True if the thing at the given URL is a directory, and False if
609
- it is a file. The URL may or may not end in '/'.
610
- """
611
- parseResult = urlparse(src_uri)
612
- otherCls = cls._findJobStoreForUrl(parseResult)
613
- return otherCls._get_is_directory(parseResult)
614
-
615
- @classmethod
616
- def list_url(cls, src_uri: str) -> list[str]:
617
- """
618
- List the directory at the given URL. Returned path components can be
619
- joined with '/' onto the passed URL to form new URLs. Those that end in
620
- '/' correspond to directories. The provided URL may or may not end with
621
- '/'.
622
-
623
- Currently supported schemes are:
624
-
625
- - 's3' for objects in Amazon S3
626
- e.g. s3://bucket/prefix/
627
-
628
- - 'file' for local files
629
- e.g. file:///local/dir/path/
630
-
631
- :param str src_uri: URL that points to a directory or prefix in the storage mechanism of a
632
- supported URL scheme e.g. a prefix in an AWS s3 bucket.
633
-
634
- :return: A list of URL components in the given directory, already URL-encoded.
635
- """
636
- parseResult = urlparse(src_uri)
637
- otherCls = cls._findJobStoreForUrl(parseResult)
638
- return otherCls._list_url(parseResult)
639
-
640
- @classmethod
641
- def read_from_url(cls, src_uri: str, writable: IO[bytes]) -> tuple[int, bool]:
642
- """
643
- Read the given URL and write its content into the given writable stream.
644
-
645
- Raises FileNotFoundError if the URL doesn't exist.
646
-
647
- :return: The size of the file in bytes and whether the executable permission bit is set
648
- """
649
- parseResult = urlparse(src_uri)
650
- otherCls = cls._findJobStoreForUrl(parseResult)
651
- return otherCls._read_from_url(parseResult, writable)
652
-
653
- @classmethod
654
- def open_url(cls, src_uri: str) -> IO[bytes]:
655
- """
656
- Read from the given URI.
657
-
658
- Raises FileNotFoundError if the URL doesn't exist.
659
-
660
- Has a readable stream interface, unlike :meth:`read_from_url` which
661
- takes a writable stream.
662
- """
663
- parseResult = urlparse(src_uri)
664
- otherCls = cls._findJobStoreForUrl(parseResult)
665
- return otherCls._open_url(parseResult)
666
-
667
- @classmethod
668
- @abstractmethod
669
- def _url_exists(cls, url: ParseResult) -> bool:
670
- """
671
- Return True if the item at the given URL exists, and Flase otherwise.
672
-
673
- May raise an error if file existence cannot be determined.
674
- """
675
- raise NotImplementedError(f"No implementation for {url}")
676
-
677
- @classmethod
678
- @abstractmethod
679
- def _get_size(cls, url: ParseResult) -> Optional[int]:
680
- """
681
- Get the size of the object at the given URL, or None if it cannot be obtained.
682
- """
683
- raise NotImplementedError(f"No implementation for {url}")
684
-
685
- @classmethod
686
- @abstractmethod
687
- def _get_is_directory(cls, url: ParseResult) -> bool:
688
- """
689
- Return True if the thing at the given URL is a directory, and False if
690
- it is a file or it is known not to exist. The URL may or may not end in
691
- '/'.
692
-
693
- :param url: URL that points to a file or object, or directory or prefix,
694
- in the storage mechanism of a supported URL scheme e.g. a blob
695
- in an AWS s3 bucket.
696
- """
697
- raise NotImplementedError(f"No implementation for {url}")
698
-
699
- @classmethod
700
- @abstractmethod
701
- def _read_from_url(cls, url: ParseResult, writable: IO[bytes]) -> tuple[int, bool]:
702
- """
703
- Reads the contents of the object at the specified location and writes it to the given
704
- writable stream.
705
-
706
- Refer to :func:`~AbstractJobStore.importFile` documentation for currently supported URL schemes.
707
-
708
- Raises FileNotFoundError if the thing at the URL is not found.
709
-
710
- :param ParseResult url: URL that points to a file or object in the storage
711
- mechanism of a supported URL scheme e.g. a blob in an AWS s3 bucket.
712
-
713
- :param IO[bytes] writable: a writable stream
714
-
715
- :return: The size of the file in bytes and whether the executable permission bit is set
716
- """
717
- raise NotImplementedError(f"No implementation for {url}")
718
-
719
- @classmethod
720
- @abstractmethod
721
- def _list_url(cls, url: ParseResult) -> list[str]:
722
- """
723
- List the contents of the given URL, which may or may not end in '/'
724
-
725
- Returns a list of URL components. Those that end in '/' are meant to be
726
- directories, while those that do not are meant to be files.
727
-
728
- Refer to :func:`~AbstractJobStore.importFile` documentation for currently supported URL schemes.
729
-
730
- :param ParseResult url: URL that points to a directory or prefix in the
731
- storage mechanism of a supported URL scheme e.g. a prefix in an AWS s3
732
- bucket.
733
-
734
- :return: The children of the given URL, already URL-encoded if
735
- appropriate. (If the URL is a bare path, no encoding is done.)
736
- """
737
- raise NotImplementedError(f"No implementation for {url}")
738
-
739
- @classmethod
740
- @abstractmethod
741
- def _open_url(cls, url: ParseResult) -> IO[bytes]:
742
- """
743
- Get a stream of the object at the specified location.
744
-
745
- Refer to :func:`~AbstractJobStore.importFile` documentation for currently supported URL schemes.
746
-
747
- Raises FileNotFoundError if the thing at the URL is not found.
748
- """
749
- raise NotImplementedError(f"No implementation for {url}")
750
-
751
- @classmethod
752
- @abstractmethod
753
- def _write_to_url(
754
- cls,
755
- readable: Union[IO[bytes], IO[str]],
756
- url: ParseResult,
757
- executable: bool = False,
758
- ) -> None:
759
- """
760
- Reads the contents of the given readable stream and writes it to the object at the
761
- specified location. Raises FileNotFoundError if the URL doesn't exist..
762
-
763
- Refer to AbstractJobStore.importFile documentation for currently supported URL schemes.
764
-
765
- :param Union[IO[bytes], IO[str]] readable: a readable stream
766
-
767
- :param ParseResult url: URL that points to a file or object in the storage
768
- mechanism of a supported URL scheme e.g. a blob in an AWS s3 bucket.
769
-
770
- :param bool executable: determines if the file has executable permissions
771
- """
772
- raise NotImplementedError(f"No implementation for {url}")
773
-
774
- @classmethod
775
- @abstractmethod
776
- def _supports_url(cls, url: ParseResult, export: bool = False) -> bool:
777
- """
778
- Returns True if the job store supports the URL's scheme.
779
-
780
- Refer to AbstractJobStore.importFile documentation for currently supported URL schemes.
781
-
782
- :param ParseResult url: a parsed URL that may be supported
783
-
784
- :param bool export: Determines if the url is supported for exported
785
-
786
- :return bool: returns true if the cls supports the URL
787
- """
788
- raise NotImplementedError(f"No implementation for {url}")
789
-
790
570
  @abstractmethod
791
571
  def destroy(self) -> None:
792
572
  """
@@ -1155,15 +935,6 @@ class AbstractJobStore(ABC):
1155
935
  """
1156
936
  raise NotImplementedError()
1157
937
 
1158
- @contextmanager
1159
- def batch(self) -> Iterator[None]:
1160
- """
1161
- If supported by the batch system, calls to create() with this context
1162
- manager active will be performed in a batch after the context manager
1163
- is released.
1164
- """
1165
- yield
1166
-
1167
938
  @deprecated(new_function_name="create_job")
1168
939
  def create(self, jobDescription: JobDescription) -> JobDescription:
1169
940
  return self.create_job(jobDescription)
@@ -1261,6 +1032,15 @@ class AbstractJobStore(ABC):
1261
1032
  def update(self, jobDescription: JobDescription) -> None:
1262
1033
  return self.update_job(jobDescription)
1263
1034
 
1035
+ @contextmanager
1036
+ def batch(self) -> Iterator[None]:
1037
+ """
1038
+ If supported by the batch system, calls to create() with this context
1039
+ manager active will be performed in a batch after the context manager
1040
+ is released.
1041
+ """
1042
+ yield
1043
+
1264
1044
  @abstractmethod
1265
1045
  def update_job(self, job_description: JobDescription) -> None:
1266
1046
  """
@@ -1431,14 +1211,14 @@ class AbstractJobStore(ABC):
1431
1211
  Creates an empty file in the job store and returns its ID.
1432
1212
  Call to fileExists(getEmptyFileStoreID(jobStoreID)) will return True.
1433
1213
 
1434
- :param str job_id: the id of a job, or None. If specified, the may be associated
1214
+ :param job_id: the id of a job, or None. If specified, the may be associated
1435
1215
  with that job in a job-store-specific way. This may influence the returned ID.
1436
1216
 
1437
- :param bool cleanup: Whether to attempt to delete the file when the job
1217
+ :param cleanup: Whether to attempt to delete the file when the job
1438
1218
  whose jobStoreID was given as jobStoreID is deleted with
1439
1219
  jobStore.delete(job). If jobStoreID was not given, does nothing.
1440
1220
 
1441
- :param str basename: If supported by the implementation, use the given
1221
+ :param basename: If supported by the implementation, use the given
1442
1222
  file basename so that when searching the job store with a query
1443
1223
  matching that basename, the file will be detected.
1444
1224
 
@@ -1872,7 +1652,7 @@ class AbstractJobStore(ABC):
1872
1652
  raise ValueError("Not a valid shared file name: '%s'." % sharedFileName)
1873
1653
 
1874
1654
 
1875
- class JobStoreSupport(AbstractJobStore, metaclass=ABCMeta):
1655
+ class JobStoreSupport(AbstractJobStore, URLAccess, metaclass=ABCMeta):
1876
1656
  """
1877
1657
  A mostly fake JobStore to access URLs not really associated with real job
1878
1658
  stores.