prefect-client 3.1.5__py3-none-any.whl → 3.1.7__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 (114) hide show
  1. prefect/__init__.py +3 -0
  2. prefect/_experimental/__init__.py +0 -0
  3. prefect/_experimental/lineage.py +181 -0
  4. prefect/_internal/compatibility/async_dispatch.py +38 -9
  5. prefect/_internal/compatibility/migration.py +1 -1
  6. prefect/_internal/concurrency/api.py +52 -52
  7. prefect/_internal/concurrency/calls.py +59 -35
  8. prefect/_internal/concurrency/cancellation.py +34 -18
  9. prefect/_internal/concurrency/event_loop.py +7 -6
  10. prefect/_internal/concurrency/threads.py +41 -33
  11. prefect/_internal/concurrency/waiters.py +28 -21
  12. prefect/_internal/pydantic/v1_schema.py +2 -2
  13. prefect/_internal/pydantic/v2_schema.py +10 -9
  14. prefect/_internal/pydantic/v2_validated_func.py +15 -10
  15. prefect/_internal/retries.py +15 -6
  16. prefect/_internal/schemas/bases.py +11 -8
  17. prefect/_internal/schemas/validators.py +7 -5
  18. prefect/_version.py +3 -3
  19. prefect/automations.py +53 -47
  20. prefect/blocks/abstract.py +12 -10
  21. prefect/blocks/core.py +148 -19
  22. prefect/blocks/system.py +2 -1
  23. prefect/cache_policies.py +11 -11
  24. prefect/client/__init__.py +3 -1
  25. prefect/client/base.py +36 -37
  26. prefect/client/cloud.py +26 -19
  27. prefect/client/collections.py +2 -2
  28. prefect/client/orchestration.py +430 -273
  29. prefect/client/schemas/__init__.py +24 -0
  30. prefect/client/schemas/actions.py +128 -121
  31. prefect/client/schemas/filters.py +1 -1
  32. prefect/client/schemas/objects.py +114 -85
  33. prefect/client/schemas/responses.py +19 -20
  34. prefect/client/schemas/schedules.py +136 -93
  35. prefect/client/subscriptions.py +30 -15
  36. prefect/client/utilities.py +46 -36
  37. prefect/concurrency/asyncio.py +6 -9
  38. prefect/concurrency/sync.py +35 -5
  39. prefect/context.py +40 -32
  40. prefect/deployments/flow_runs.py +6 -8
  41. prefect/deployments/runner.py +14 -14
  42. prefect/deployments/steps/core.py +3 -1
  43. prefect/deployments/steps/pull.py +60 -12
  44. prefect/docker/__init__.py +1 -1
  45. prefect/events/clients.py +55 -4
  46. prefect/events/filters.py +1 -1
  47. prefect/events/related.py +2 -1
  48. prefect/events/schemas/events.py +26 -21
  49. prefect/events/utilities.py +3 -2
  50. prefect/events/worker.py +8 -0
  51. prefect/filesystems.py +3 -3
  52. prefect/flow_engine.py +87 -87
  53. prefect/flow_runs.py +7 -5
  54. prefect/flows.py +218 -176
  55. prefect/logging/configuration.py +1 -1
  56. prefect/logging/highlighters.py +1 -2
  57. prefect/logging/loggers.py +30 -20
  58. prefect/main.py +17 -24
  59. prefect/results.py +43 -22
  60. prefect/runner/runner.py +43 -21
  61. prefect/runner/server.py +30 -32
  62. prefect/runner/storage.py +3 -3
  63. prefect/runner/submit.py +3 -6
  64. prefect/runner/utils.py +6 -6
  65. prefect/runtime/flow_run.py +7 -0
  66. prefect/serializers.py +28 -24
  67. prefect/settings/constants.py +2 -2
  68. prefect/settings/legacy.py +1 -1
  69. prefect/settings/models/experiments.py +5 -0
  70. prefect/settings/models/server/events.py +10 -0
  71. prefect/task_engine.py +87 -26
  72. prefect/task_runners.py +2 -2
  73. prefect/task_worker.py +43 -25
  74. prefect/tasks.py +148 -142
  75. prefect/telemetry/bootstrap.py +15 -2
  76. prefect/telemetry/instrumentation.py +1 -1
  77. prefect/telemetry/processors.py +10 -7
  78. prefect/telemetry/run_telemetry.py +231 -0
  79. prefect/transactions.py +14 -14
  80. prefect/types/__init__.py +5 -5
  81. prefect/utilities/_engine.py +96 -0
  82. prefect/utilities/annotations.py +25 -18
  83. prefect/utilities/asyncutils.py +126 -140
  84. prefect/utilities/callables.py +87 -78
  85. prefect/utilities/collections.py +278 -117
  86. prefect/utilities/compat.py +13 -21
  87. prefect/utilities/context.py +6 -5
  88. prefect/utilities/dispatch.py +23 -12
  89. prefect/utilities/dockerutils.py +33 -32
  90. prefect/utilities/engine.py +126 -239
  91. prefect/utilities/filesystem.py +18 -15
  92. prefect/utilities/hashing.py +10 -11
  93. prefect/utilities/importtools.py +40 -27
  94. prefect/utilities/math.py +9 -5
  95. prefect/utilities/names.py +3 -3
  96. prefect/utilities/processutils.py +121 -57
  97. prefect/utilities/pydantic.py +41 -36
  98. prefect/utilities/render_swagger.py +22 -12
  99. prefect/utilities/schema_tools/__init__.py +2 -1
  100. prefect/utilities/schema_tools/hydration.py +50 -43
  101. prefect/utilities/schema_tools/validation.py +52 -42
  102. prefect/utilities/services.py +13 -12
  103. prefect/utilities/templating.py +45 -45
  104. prefect/utilities/text.py +2 -1
  105. prefect/utilities/timeout.py +4 -4
  106. prefect/utilities/urls.py +9 -4
  107. prefect/utilities/visualization.py +46 -24
  108. prefect/variables.py +136 -27
  109. prefect/workers/base.py +15 -8
  110. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/METADATA +5 -2
  111. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/RECORD +114 -110
  112. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/LICENSE +0 -0
  113. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/WHEEL +0 -0
  114. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/top_level.txt +0 -0
@@ -99,7 +99,9 @@ def _get_function_for_step(
99
99
  return step_func
100
100
 
101
101
 
102
- async def run_step(step: Dict, upstream_outputs: Optional[Dict] = None) -> Dict:
102
+ async def run_step(
103
+ step: dict[str, Any], upstream_outputs: Optional[dict[str, Any]] = None
104
+ ) -> dict[str, Any]:
103
105
  """
104
106
  Runs a step, returns the step's output.
105
107
 
@@ -6,10 +6,11 @@ import os
6
6
  from pathlib import Path
7
7
  from typing import TYPE_CHECKING, Any, Optional
8
8
 
9
+ from prefect._internal.compatibility.async_dispatch import async_dispatch
9
10
  from prefect._internal.retries import retry_async_fn
10
11
  from prefect.logging.loggers import get_logger
11
12
  from prefect.runner.storage import BlockStorageAdapter, GitRepository, RemoteStorage
12
- from prefect.utilities.asyncutils import sync_compatible
13
+ from prefect.utilities.asyncutils import run_coro_as_sync
13
14
 
14
15
  deployment_logger = get_logger("deployment")
15
16
 
@@ -17,7 +18,7 @@ if TYPE_CHECKING:
17
18
  from prefect.blocks.core import Block
18
19
 
19
20
 
20
- def set_working_directory(directory: str) -> dict:
21
+ def set_working_directory(directory: str) -> dict[str, str]:
21
22
  """
22
23
  Sets the working directory; works with both absolute and relative paths.
23
24
 
@@ -37,15 +38,64 @@ def set_working_directory(directory: str) -> dict:
37
38
  base_delay=1,
38
39
  max_delay=10,
39
40
  retry_on_exceptions=(RuntimeError,),
41
+ operation_name="git_clone",
40
42
  )
41
- @sync_compatible
42
- async def git_clone(
43
+ async def _pull_git_repository_with_retries(repo: GitRepository):
44
+ await repo.pull_code()
45
+
46
+
47
+ async def agit_clone(
48
+ repository: str,
49
+ branch: Optional[str] = None,
50
+ include_submodules: bool = False,
51
+ access_token: Optional[str] = None,
52
+ credentials: Optional["Block"] = None,
53
+ ) -> dict[str, str]:
54
+ """
55
+ Asynchronously clones a git repository into the current working directory.
56
+
57
+ Args:
58
+ repository: the URL of the repository to clone
59
+ branch: the branch to clone; if not provided, the default branch will be used
60
+ include_submodules (bool): whether to include git submodules when cloning the repository
61
+ access_token: an access token to use for cloning the repository; if not provided
62
+ the repository will be cloned using the default git credentials
63
+ credentials: a GitHubCredentials, GitLabCredentials, or BitBucketCredentials block can be used to specify the
64
+ credentials to use for cloning the repository.
65
+
66
+ Returns:
67
+ dict: a dictionary containing a `directory` key of the new directory that was created
68
+
69
+ Raises:
70
+ subprocess.CalledProcessError: if the git clone command fails for any reason
71
+ """
72
+ if access_token and credentials:
73
+ raise ValueError(
74
+ "Please provide either an access token or credentials but not both."
75
+ )
76
+
77
+ _credentials = {"access_token": access_token} if access_token else credentials
78
+
79
+ storage = GitRepository(
80
+ url=repository,
81
+ credentials=_credentials,
82
+ branch=branch,
83
+ include_submodules=include_submodules,
84
+ )
85
+
86
+ await _pull_git_repository_with_retries(storage)
87
+
88
+ return dict(directory=str(storage.destination.relative_to(Path.cwd())))
89
+
90
+
91
+ @async_dispatch(agit_clone)
92
+ def git_clone(
43
93
  repository: str,
44
94
  branch: Optional[str] = None,
45
95
  include_submodules: bool = False,
46
96
  access_token: Optional[str] = None,
47
97
  credentials: Optional["Block"] = None,
48
- ) -> dict:
98
+ ) -> dict[str, str]:
49
99
  """
50
100
  Clones a git repository into the current working directory.
51
101
 
@@ -120,20 +170,18 @@ async def git_clone(
120
170
  "Please provide either an access token or credentials but not both."
121
171
  )
122
172
 
123
- credentials = {"access_token": access_token} if access_token else credentials
173
+ _credentials = {"access_token": access_token} if access_token else credentials
124
174
 
125
175
  storage = GitRepository(
126
176
  url=repository,
127
- credentials=credentials,
177
+ credentials=_credentials,
128
178
  branch=branch,
129
179
  include_submodules=include_submodules,
130
180
  )
131
181
 
132
- await storage.pull_code()
182
+ run_coro_as_sync(_pull_git_repository_with_retries(storage))
133
183
 
134
- directory = str(storage.destination.relative_to(Path.cwd()))
135
- deployment_logger.info(f"Cloned repository {repository!r} into {directory!r}")
136
- return {"directory": directory}
184
+ return dict(directory=str(storage.destination.relative_to(Path.cwd())))
137
185
 
138
186
 
139
187
  async def pull_from_remote_storage(url: str, **settings: Any):
@@ -190,7 +238,7 @@ async def pull_with_block(block_document_name: str, block_type_slug: str):
190
238
 
191
239
  full_slug = f"{block_type_slug}/{block_document_name}"
192
240
  try:
193
- block = await Block.load(full_slug)
241
+ block = await Block.aload(full_slug)
194
242
  except Exception:
195
243
  deployment_logger.exception("Unable to load block '%s'", full_slug)
196
244
  raise
@@ -17,4 +17,4 @@ def __getattr__(name: str) -> object:
17
17
  module, attr = _public_api[name]
18
18
  return getattr(import_module(module), attr)
19
19
 
20
- raise ImportError(f"module {__name__!r} has no attribute {name!r}")
20
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
prefect/events/clients.py CHANGED
@@ -1,11 +1,13 @@
1
1
  import abc
2
2
  import asyncio
3
+ import os
3
4
  from types import TracebackType
4
5
  from typing import (
5
6
  TYPE_CHECKING,
6
7
  Any,
7
8
  ClassVar,
8
9
  Dict,
10
+ Generator,
9
11
  List,
10
12
  MutableMapping,
11
13
  Optional,
@@ -13,20 +15,22 @@ from typing import (
13
15
  Type,
14
16
  cast,
15
17
  )
18
+ from urllib.parse import urlparse
16
19
  from uuid import UUID
17
20
 
18
21
  import orjson
19
22
  import pendulum
20
23
  from cachetools import TTLCache
21
24
  from prometheus_client import Counter
25
+ from python_socks.async_.asyncio import Proxy
22
26
  from typing_extensions import Self
23
27
  from websockets import Subprotocol
24
- from websockets.client import WebSocketClientProtocol, connect
25
28
  from websockets.exceptions import (
26
29
  ConnectionClosed,
27
30
  ConnectionClosedError,
28
31
  ConnectionClosedOK,
29
32
  )
33
+ from websockets.legacy.client import Connect, WebSocketClientProtocol
30
34
 
31
35
  from prefect.events import Event
32
36
  from prefect.logging import get_logger
@@ -80,6 +84,53 @@ def events_out_socket_from_api_url(url: str):
80
84
  return http_to_ws(url) + "/events/out"
81
85
 
82
86
 
87
+ class WebsocketProxyConnect(Connect):
88
+ def __init__(self: Self, uri: str, **kwargs: Any):
89
+ # super() is intentionally deferred to the _proxy_connect method
90
+ # to allow for the socket to be established first
91
+
92
+ self.uri = uri
93
+ self._kwargs = kwargs
94
+
95
+ u = urlparse(uri)
96
+ host = u.hostname
97
+
98
+ if u.scheme == "ws":
99
+ port = u.port or 80
100
+ proxy_url = os.environ.get("HTTP_PROXY")
101
+ elif u.scheme == "wss":
102
+ port = u.port or 443
103
+ proxy_url = os.environ.get("HTTPS_PROXY")
104
+ kwargs["server_hostname"] = host
105
+ else:
106
+ raise ValueError(
107
+ "Unsupported scheme %s. Expected 'ws' or 'wss'. " % u.scheme
108
+ )
109
+
110
+ self._proxy = Proxy.from_url(proxy_url) if proxy_url else None
111
+ self._host = host
112
+ self._port = port
113
+
114
+ async def _proxy_connect(self: Self) -> WebSocketClientProtocol:
115
+ if self._proxy:
116
+ sock = await self._proxy.connect(
117
+ dest_host=self._host,
118
+ dest_port=self._port,
119
+ )
120
+ self._kwargs["sock"] = sock
121
+
122
+ super().__init__(self.uri, **self._kwargs)
123
+ proto = await self.__await_impl__()
124
+ return proto
125
+
126
+ def __await__(self: Self) -> Generator[Any, None, WebSocketClientProtocol]:
127
+ return self._proxy_connect().__await__()
128
+
129
+
130
+ def websocket_connect(uri: str, **kwargs: Any) -> WebsocketProxyConnect:
131
+ return WebsocketProxyConnect(uri, **kwargs)
132
+
133
+
83
134
  def get_events_client(
84
135
  reconnection_attempts: int = 10,
85
136
  checkpoint_every: int = 700,
@@ -265,7 +316,7 @@ class PrefectEventsClient(EventsClient):
265
316
  )
266
317
 
267
318
  self._events_socket_url = events_in_socket_from_api_url(api_url)
268
- self._connect = connect(self._events_socket_url)
319
+ self._connect = websocket_connect(self._events_socket_url)
269
320
  self._websocket = None
270
321
  self._reconnection_attempts = reconnection_attempts
271
322
  self._unconfirmed_events = []
@@ -435,7 +486,7 @@ class PrefectCloudEventsClient(PrefectEventsClient):
435
486
  reconnection_attempts=reconnection_attempts,
436
487
  checkpoint_every=checkpoint_every,
437
488
  )
438
- self._connect = connect(
489
+ self._connect = websocket_connect(
439
490
  self._events_socket_url,
440
491
  extra_headers={"Authorization": f"bearer {api_key}"},
441
492
  )
@@ -494,7 +545,7 @@ class PrefectEventSubscriber:
494
545
 
495
546
  logger.debug("Connecting to %s", socket_url)
496
547
 
497
- self._connect = connect(
548
+ self._connect = websocket_connect(
498
549
  socket_url,
499
550
  subprotocols=[Subprotocol("prefect")],
500
551
  )
prefect/events/filters.py CHANGED
@@ -3,9 +3,9 @@ from uuid import UUID
3
3
 
4
4
  import pendulum
5
5
  from pydantic import Field, PrivateAttr
6
- from pydantic_extra_types.pendulum_dt import DateTime
7
6
 
8
7
  from prefect._internal.schemas.bases import PrefectBaseModel
8
+ from prefect.types import DateTime
9
9
  from prefect.utilities.collections import AutoEnum
10
10
 
11
11
  from .schemas.events import Event, Resource, ResourceSpecification
prefect/events/related.py CHANGED
@@ -15,7 +15,8 @@ from typing import (
15
15
  from uuid import UUID
16
16
 
17
17
  import pendulum
18
- from pendulum.datetime import DateTime
18
+
19
+ from prefect.types import DateTime
19
20
 
20
21
  from .schemas.events import RelatedResource
21
22
 
@@ -13,17 +13,21 @@ from typing import (
13
13
  )
14
14
  from uuid import UUID, uuid4
15
15
 
16
- import pendulum
17
- from pydantic import ConfigDict, Field, RootModel, field_validator, model_validator
18
- from pydantic_extra_types.pendulum_dt import DateTime
19
- from typing_extensions import Self
16
+ from pydantic import (
17
+ AfterValidator,
18
+ ConfigDict,
19
+ Field,
20
+ RootModel,
21
+ model_validator,
22
+ )
23
+ from typing_extensions import Annotated, Self
20
24
 
21
25
  from prefect._internal.schemas.bases import PrefectBaseModel
22
26
  from prefect.logging import get_logger
23
27
  from prefect.settings import (
24
28
  PREFECT_EVENTS_MAXIMUM_LABELS_PER_RESOURCE,
25
- PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES,
26
29
  )
30
+ from prefect.types import DateTime
27
31
 
28
32
  from .labelling import Labelled
29
33
 
@@ -90,22 +94,34 @@ class RelatedResource(Resource):
90
94
  return self["prefect.resource.role"]
91
95
 
92
96
 
97
+ def _validate_related_resources(value) -> List:
98
+ from prefect.settings import PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES
99
+
100
+ if len(value) > PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES.value():
101
+ raise ValueError(
102
+ "The maximum number of related resources "
103
+ f"is {PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES.value()}"
104
+ )
105
+ return value
106
+
107
+
93
108
  class Event(PrefectBaseModel):
94
109
  """The client-side view of an event that has happened to a Resource"""
95
110
 
96
111
  model_config = ConfigDict(extra="ignore")
97
112
 
98
113
  occurred: DateTime = Field(
99
- default_factory=lambda: pendulum.now("UTC"),
114
+ default_factory=lambda: DateTime.now("UTC"),
100
115
  description="When the event happened from the sender's perspective",
101
116
  )
102
- event: str = Field(
103
- description="The name of the event that happened",
104
- )
117
+ event: str = Field(description="The name of the event that happened")
105
118
  resource: Resource = Field(
106
119
  description="The primary Resource this event concerns",
107
120
  )
108
- related: List[RelatedResource] = Field(
121
+ related: Annotated[
122
+ List[RelatedResource],
123
+ AfterValidator(_validate_related_resources),
124
+ ] = Field(
109
125
  default_factory=list,
110
126
  description="A list of additional Resources involved in this event",
111
127
  )
@@ -144,17 +160,6 @@ class Event(PrefectBaseModel):
144
160
  resources[related.role].append(related)
145
161
  return resources
146
162
 
147
- @field_validator("related")
148
- @classmethod
149
- def enforce_maximum_related_resources(cls, value: List[RelatedResource]):
150
- if len(value) > PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES.value():
151
- raise ValueError(
152
- "The maximum number of related resources "
153
- f"is {PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES.value()}"
154
- )
155
-
156
- return value
157
-
158
163
  def find_resource_label(self, label: str) -> Optional[str]:
159
164
  """Finds the value of the given label in this event's resource or one of its
160
165
  related resources. If the label starts with `related:<role>:`, search for the
@@ -3,7 +3,6 @@ from typing import Any, Dict, List, Optional, Union
3
3
  from uuid import UUID
4
4
 
5
5
  import pendulum
6
- from pydantic_extra_types.pendulum_dt import DateTime
7
6
 
8
7
  from .clients import (
9
8
  AssertingEventsClient,
@@ -20,11 +19,12 @@ TIGHT_TIMING = timedelta(minutes=5)
20
19
  def emit_event(
21
20
  event: str,
22
21
  resource: Dict[str, str],
23
- occurred: Optional[DateTime] = None,
22
+ occurred: Optional[pendulum.DateTime] = None,
24
23
  related: Optional[Union[List[Dict[str, str]], List[RelatedResource]]] = None,
25
24
  payload: Optional[Dict[str, Any]] = None,
26
25
  id: Optional[UUID] = None,
27
26
  follows: Optional[Event] = None,
27
+ **kwargs: Optional[Dict[str, Any]],
28
28
  ) -> Optional[Event]:
29
29
  """
30
30
  Send an event to Prefect Cloud.
@@ -63,6 +63,7 @@ def emit_event(
63
63
  event_kwargs: Dict[str, Any] = {
64
64
  "event": event,
65
65
  "resource": resource,
66
+ **kwargs,
66
67
  }
67
68
 
68
69
  if occurred is None:
prefect/events/worker.py CHANGED
@@ -83,6 +83,14 @@ class EventsWorker(QueueService[Event]):
83
83
  await self._client.emit(event)
84
84
 
85
85
  async def attach_related_resources_from_context(self, event: Event):
86
+ if "prefect.resource.lineage-group" in event.resource:
87
+ # We attach related resources to lineage events in `emit_lineage_event`,
88
+ # instead of the worker, because not all run-related resources are
89
+ # upstream from every lineage event (they might be downstream).
90
+ # The "related" field in the event schema tracks upstream resources
91
+ # only.
92
+ return
93
+
86
94
  exclude = {resource.id for resource in event.involved_resources}
87
95
  event.related += await related_resources_from_run_context(
88
96
  client=self._orchestration_client, exclude=exclude
prefect/filesystems.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import abc
2
2
  import urllib.parse
3
3
  from pathlib import Path
4
+ from shutil import copytree
4
5
  from typing import Any, Dict, Optional
5
6
 
6
7
  import anyio
@@ -13,7 +14,6 @@ from prefect._internal.schemas.validators import (
13
14
  )
14
15
  from prefect.blocks.core import Block
15
16
  from prefect.utilities.asyncutils import run_sync_in_worker_thread, sync_compatible
16
- from prefect.utilities.compat import copytree
17
17
  from prefect.utilities.filesystem import filter_files
18
18
 
19
19
  from ._internal.compatibility.migration import getattr_migration
@@ -158,7 +158,7 @@ class LocalFileSystem(WritableFileSystem, WritableDeploymentStorage):
158
158
  copytree(from_path, local_path, dirs_exist_ok=True, ignore=ignore_func)
159
159
 
160
160
  async def _get_ignore_func(self, local_path: str, ignore_file: str):
161
- with open(ignore_file, "r") as f:
161
+ with open(ignore_file) as f:
162
162
  ignore_patterns = f.readlines()
163
163
  included_files = filter_files(root=local_path, ignore_patterns=ignore_patterns)
164
164
 
@@ -348,7 +348,7 @@ class RemoteFileSystem(WritableFileSystem, WritableDeploymentStorage):
348
348
 
349
349
  included_files = None
350
350
  if ignore_file:
351
- with open(ignore_file, "r") as f:
351
+ with open(ignore_file) as f:
352
352
  ignore_patterns = f.readlines()
353
353
 
354
354
  included_files = filter_files(