Skip to content

Commit 877bf58

Browse files
committed
Do not add package_uid to for_package if it exists
* Add test for extracted pypi wheel * Update test expectations Signed-off-by: Jono Yang <jyang@nexb.com>
1 parent 6e8fd1f commit 877bf58

11 files changed

Lines changed: 808 additions & 53 deletions

File tree

src/packagedcode/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,9 +805,12 @@ def compute_normalized_license(declared_license, expression_symbols=None):
805805

806806
def add_to_package(package_uid, resource, codebase):
807807
"""
808-
Append `package_uid` to `resource.for_packages`, if the attribute exists.
808+
Append `package_uid` to `resource.for_packages`, if the attribute exists and
809+
`package_uid` is not already in `resource.for_packages`.
809810
"""
810811
if hasattr(resource, 'for_packages') and isinstance(resource.for_packages, list):
812+
if package_uid in resource.for_packages:
813+
return
811814
resource.for_packages.append(package_uid)
812815
resource.save(codebase)
813816

tests/packagedcode/data/debian/debian-container-layer.tar.xz.get-installed-expected.json

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -359,58 +359,6 @@
359359
}
360360
],
361361
"for_packages": [
362-
"pkg:deb/libndp0@1.4-2ubuntu0.16.04.1?architecture=amd64&uuid=fixed-uid-done-for-testing-5642512d1758",
363-
"pkg:deb/libndp0@1.4-2ubuntu0.16.04.1?architecture=amd64&uuid=fixed-uid-done-for-testing-5642512d1758"
364-
],
365-
"scan_errors": []
366-
},
367-
{
368-
"path": "debian-container-layer.tar.xz/usr/share/doc/libndp0/copyright",
369-
"type": "file",
370-
"package_data": [
371-
{
372-
"type": "deb",
373-
"namespace": null,
374-
"name": "libndp0",
375-
"version": null,
376-
"qualifiers": {},
377-
"subpath": null,
378-
"primary_language": null,
379-
"description": null,
380-
"release_date": null,
381-
"parties": [],
382-
"keywords": [],
383-
"homepage_url": null,
384-
"download_url": null,
385-
"size": null,
386-
"sha1": null,
387-
"md5": null,
388-
"sha256": null,
389-
"sha512": null,
390-
"bug_tracking_url": null,
391-
"code_view_url": null,
392-
"vcs_url": null,
393-
"copyright": "Copyright 2013 Jiri Pirko <jiri@resnulli.us>\nCopyright 2014 Andrew Ayer <agwa@andrewayer.name>",
394-
"license_expression": "(lgpl-2.1-plus AND lgpl-2.1-plus AND lgpl-2.1) AND (lgpl-2.1-plus AND lgpl-2.1-plus AND lgpl-2.1)",
395-
"declared_license": [
396-
"LGPL-2.1+",
397-
"LGPL-2.1+",
398-
"LGPL-2.1+"
399-
],
400-
"notice_text": null,
401-
"source_packages": [],
402-
"file_references": [],
403-
"extra_data": {},
404-
"dependencies": [],
405-
"repository_homepage_url": null,
406-
"repository_download_url": null,
407-
"api_data_url": null,
408-
"datasource_id": "debian_copyright_in_package",
409-
"purl": "pkg:deb/libndp0"
410-
}
411-
],
412-
"for_packages": [
413-
"pkg:deb/libndp0@1.4-2ubuntu0.16.04.1?architecture=amd64&uuid=fixed-uid-done-for-testing-5642512d1758",
414362
"pkg:deb/libndp0@1.4-2ubuntu0.16.04.1?architecture=amd64&uuid=fixed-uid-done-for-testing-5642512d1758"
415363
],
416364
"scan_errors": []

tests/packagedcode/data/pypi/unpacked_wheel/daglib_wheel_extracted-expected.json

Lines changed: 368 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2022 Michael Harris
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
Metadata-Version: 2.1
2+
Name: daglib
3+
Version: 0.6.0
4+
Summary: Lightweight DAG composition framework
5+
Home-page: https://github.com/mharrisb1/daglib
6+
License: MIT
7+
Author: Michael Harris
8+
Author-email: mharris@luabase.com
9+
Requires-Python: >=3.10,<4.0
10+
Classifier: Intended Audience :: Developers
11+
Classifier: License :: OSI Approved :: MIT License
12+
Classifier: Programming Language :: Python :: 3
13+
Classifier: Programming Language :: Python :: 3.10
14+
Provides-Extra: graphviz
15+
Provides-Extra: ipycytoscape
16+
Requires-Dist: dask[delayed] (>=2022.7.1,<2023.0.0)
17+
Requires-Dist: graphviz (>=0.20,<0.21); extra == "graphviz"
18+
Requires-Dist: ipycytoscape (>=1.3.3,<2.0.0); extra == "ipycytoscape"
19+
Requires-Dist: networkx (>=2.8.5,<3.0.0)
20+
Project-URL: Documentation, https://mharrisb1.github.io/daglib/
21+
Project-URL: Repository, https://github.com/mharrisb1/daglib
22+
Description-Content-Type: text/markdown
23+
24+
# ⚗️ Daglib - Lightweight DAG composition framework
25+
26+
[![PyPI version](https://badge.fury.io/py/daglib.svg)](https://badge.fury.io/py/daglib)
27+
[![PyPI - Downloads](https://img.shields.io/pypi/dm/daglib)](https://pypi.org/project/daglib/)
28+
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/daglib.svg)](https://pypi.org/project/daglib/)
29+
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
30+
[![Checked with mypy](https://img.shields.io/badge/mypy-checked-blue.svg)](https://mypy.readthedocs.io/en/stable/)
31+
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
32+
33+
Daglib is a lightweight, embeddable parallel task execution library used for turning pure Python functions into executable task graphs.
34+
35+
# Installation
36+
37+
Core
38+
39+
```shell
40+
pip install daglib
41+
```
42+
43+
With visualizations enabled
44+
45+
```shell
46+
pip install 'daglib[graphviz]' # static visualizations
47+
# or
48+
pip install 'daglib[ipycytoscape]' # interactive visulizations
49+
```
50+
51+
# Create your first DAG
52+
53+
54+
```python
55+
import daglib
56+
57+
dag = daglib.Dag()
58+
59+
60+
@dag.task()
61+
def task_1a():
62+
return "Hello"
63+
64+
65+
@dag.task()
66+
def task_1b():
67+
return "world!"
68+
69+
70+
@dag.task()
71+
def task_2(task_1a, task_1b):
72+
return f"{task_1a}, {task_1b}"
73+
74+
75+
dag.run()
76+
```
77+
78+
79+
80+
81+
'Hello, world!'
82+
83+
84+
85+
# Beyond the "Hello, world!" example
86+
87+
For a more involved example, we will create a small pipeline that takes data from four source tables and creates a single reporting table. The data is driver-level information from the current 2022 Formula 1 season. The output will be a pivot table for team-level metrics.
88+
89+
## Source Tables
90+
91+
1. Team - Team of driver
92+
2. Points - Current total Driver's World Championship points for each driver for the season
93+
3. Wins - Current number of wins for each driver for the season
94+
4. Podiums - Current number of times the driver finished in the top 3 for the season
95+
96+
97+
```python
98+
import pandas as pd
99+
import daglib
100+
101+
# Ignore. Used to render the DataFrame correctly in the README
102+
pd.set_option("display.notebook_repr_html", False)
103+
104+
dag = daglib.Dag()
105+
106+
107+
@dag.task()
108+
def team():
109+
return pd.DataFrame(dict(
110+
driver=["Max", "Charles", "Lewis", "Sergio", "Carlos", "George"],
111+
team=["Red Bull", "Ferrari", "Mercedes", "Red Bull", "Ferrari", "Mercedes"],
112+
)).set_index("driver")
113+
114+
115+
@dag.task()
116+
def points():
117+
return pd.DataFrame(dict(
118+
driver=["Max", "Charles", "Lewis", "Sergio", "Carlos", "George"],
119+
points=[258, 178, 146, 173, 156, 158]
120+
)).set_index("driver")
121+
122+
123+
@dag.task()
124+
def wins():
125+
return pd.DataFrame(dict(
126+
driver=["Max", "Charles", "Lewis", "Sergio", "Carlos", "George"],
127+
wins=[8, 3, 0, 1, 1, 0]
128+
)).set_index("driver")
129+
130+
131+
@dag.task()
132+
def podiums():
133+
return pd.DataFrame(dict(
134+
driver=["Max", "Charles", "Lewis", "Sergio", "Carlos", "George"],
135+
podiums=[10, 5, 6, 6, 6, 5]
136+
)).set_index("driver")
137+
138+
139+
@dag.task()
140+
def driver_metrics(team, points, wins, podiums):
141+
return team.join(points).join(wins).join(podiums)
142+
143+
144+
@dag.task()
145+
def team_metrics(driver_metrics):
146+
return driver_metrics.groupby("team").sum().sort_values("points", ascending=False)
147+
148+
149+
dag.run()
150+
```
151+
152+
153+
154+
155+
points wins podiums
156+
team
157+
Red Bull 431 9 16
158+
Ferrari 334 4 11
159+
Mercedes 304 0 11
160+
161+
162+
163+
## Task Graph Visualization
164+
165+
The DAG we created above will create a task graph that looks like the following
166+
167+
![task graph](https://storage.googleapis.com/daglib-image-assets/example-dag.png)
168+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
daglib/__init__.py,sha256=zZChUiwWHPYMiYHWpak0jHMt01MrLaKjtQjdHjqKZ3M,72
2+
daglib/dag.py,sha256=29Vm4DiQocV6tSVu9pZJUZPmqc-Q7EujHU7xD4qPJaY,2761
3+
daglib/task.py,sha256=ouWf979IU6OKgpj6j4301B2wo46wkTKjkkFr2Jp-xj0,4531
4+
daglib-0.6.0.dist-info/LICENSE,sha256=-TUoNdFfc-LQG650e2zjOGojvi-3PAbfeZ-6l6W8mKI,1058
5+
daglib-0.6.0.dist-info/WHEEL,sha256=DA86_h4QwwzGeRoz62o1svYt5kGEXpoUTuTtwzoTb30,83
6+
daglib-0.6.0.dist-info/METADATA,sha256=8BukUSgAbtR0jO-L0ef2a9S1-G8aZX4_Pk_uOk0yjPI,4493
7+
daglib-0.6.0.dist-info/RECORD,,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Wheel-Version: 1.0
2+
Generator: poetry 1.0.8
3+
Root-Is-Purelib: true
4+
Tag: py3-none-any
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .dag import Dag
2+
from .task import Task, Arg
3+
4+
__version__ = "0.5.0"
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from __future__ import annotations
2+
3+
import uuid
4+
from typing import Any, Callable
5+
6+
import networkx as nx
7+
from dask.delayed import Delayed
8+
from dask.optimization import cull
9+
10+
from daglib.task import Task, Arg
11+
12+
13+
class Dag:
14+
def __init__(self, name: str = uuid.uuid4().hex, description: str = "") -> None:
15+
self.name = "".join(x for x in name if x.isalnum()).lower()
16+
self.description = description
17+
self._tasks_by_name: dict[str, Task] = {}
18+
self.nxg = nx.DiGraph()
19+
20+
@property
21+
def run_id(self) -> str:
22+
return f"run_{uuid.uuid1().hex}"
23+
24+
def add_subdag(self, other: Dag) -> None:
25+
self._tasks_by_name |= other._tasks_by_name
26+
27+
def register_task(self, task: Task) -> Task:
28+
self._tasks_by_name[task.name] = task
29+
return task
30+
31+
def register_task_from_function(
32+
self, fn: Callable[..., Any], name: str | None = None, args: list[Arg] | None = None
33+
) -> Callable[..., Any]:
34+
task = Task.from_function(fn, name=name, args=args)
35+
self.register_task(task)
36+
return fn
37+
38+
def task(self) -> Any:
39+
def register(fn: Callable[..., Any]) -> Callable[..., Any]:
40+
return self.register_task_from_function(fn)
41+
42+
return register
43+
44+
def _build_graph(self) -> None:
45+
edges = [(self._tasks_by_name[arg.name], task) for task in self._tasks_by_name.values() for arg in task.args]
46+
self.nxg = nx.DiGraph(edges)
47+
48+
@property
49+
def _dsk(self) -> dict[str, tuple[Any, ...]]:
50+
return {task.name: tuple([task.fn, *[arg.name for arg in task.args]]) for task in nx.topological_sort(self.nxg)}
51+
52+
@property
53+
def _keys(self) -> list[str]:
54+
return [task.name for task in self.nxg.nodes if not list(self.nxg.successors(task))]
55+
56+
def materialize(self, to_step: str | Callable[..., Any] | None = None, optimize: bool = False) -> Delayed:
57+
self._build_graph()
58+
keys: list[str] | str = self._keys
59+
if len(keys) == 1:
60+
keys = keys[0]
61+
dsk = self._dsk
62+
if to_step:
63+
if callable(to_step):
64+
keys = to_step.__name__
65+
optimize = True
66+
if optimize:
67+
layers, _ = cull(dsk, keys)
68+
return Delayed(keys, dsk)
69+
70+
def run(self, to_step: str | Callable[..., Any] | None = None, optimize: bool = False) -> Any:
71+
return self.materialize(to_step, optimize).compute()
72+
73+
# noinspection PyShadowingBuiltins
74+
def visualize(
75+
self,
76+
to_step: str | Callable[..., Any] | None = None,
77+
optimize: bool = False,
78+
filename: str | None = None,
79+
format: str | None = None,
80+
**kwargs: Any,
81+
) -> Any:
82+
return self.materialize(to_step, optimize).visualize(filename, format, **kwargs)

0 commit comments

Comments
 (0)