Skip to content

Commit 703e4f4

Browse files
committed
Make fetching and saving license references work
Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
1 parent 7de8e51 commit 703e4f4

5 files changed

Lines changed: 69 additions & 63 deletions

File tree

src/attributecode/api.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import json
2222

23+
import click
24+
2325
from attributecode import ERROR
2426
from attributecode import Error
2527
from attributecode import util
@@ -81,6 +83,7 @@ def request_license_data(api_url, api_key, license_key):
8183
response_content = response.read().decode('utf-8')
8284
# FIXME: this should be an ordered dict
8385
license_data = json.loads(response_content)
86+
8487
if not license_data['results']:
8588
msg = u"Invalid 'license': %s" % license_key
8689
errors.append(Error(ERROR, msg))
@@ -127,26 +130,25 @@ def get_license_details(api_url, api_key, license_key):
127130
return lic, errors
128131

129132

130-
def fetch_licenses(abouts, api_url, api_key):
133+
def fetch_licenses(abouts, api_url, api_key, verbose=False):
131134
"""
132135
Return a mapping of {license key: License object} given an `abouts` list of
133136
About object and a list of Error.
134137
"""
135138
dje_domain = '{uri.scheme}://{uri.netloc}/'.format(uri=urlparse(api_url))
136139
dje_license_url = urljoin(dje_domain, 'urn/?urn=urn:dje:license:{license_key}')
137140

138-
licenses_by_key = {}
139141
errors = []
140142

141143
if have_network_connection():
142144
if not valid_api_url(api_url):
143-
msg = "URL not reachable. Invalid '--api_url'. License generation is skipped."
145+
msg = "URL not reachable. Invalid '--api_url'. License retrieval is skipped."
144146
errors.append(Error(ERROR, msg))
145147
else:
146-
msg = 'Network problem. Please check your Internet connection. License generation is skipped.'
148+
msg = 'Network problem. Please check your Internet connection. License retrieval is skipped.'
147149
errors.append(Error(ERROR, msg))
148150

149-
msg = "Authorization denied. Invalid '--api_key'. License generation is skipped."
151+
msg = "Authorization denied. Invalid '--api_key'. License retrieval is skipped."
150152
auth_error = Error(ERROR, msg)
151153

152154
# collect unique license keys
@@ -160,18 +162,21 @@ def fetch_licenses(abouts, api_url, api_key):
160162
about.license_expression, unique=True, simple=True)
161163
license_keys.update(about_keys)
162164

165+
licenses_by_key = {}
166+
163167
# fetch license key proper
164-
for license_key in license_keys:
168+
for license_key in sorted(license_keys):
165169
# No need to go through fetching all the licensesif we detected invalid '--api_key'
166170
if auth_error in errors:
167171
break
168-
169172
license, errs = get_license_details(api_url, api_key, license_key) #NOQA
170173
errors.extend(errs)
171174
if license:
172175
license.url = dje_license_url.format(license_key=license_key)
173-
licenses_by_key[license_key] = license_key
176+
licenses_by_key[license_key] = license
174177

178+
if verbose:
179+
click.echo('Fetched license: {}'.format(license_key))
175180

176181
return licenses_by_key, util.unique(errors)
177182

src/attributecode/cmd.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,22 +274,24 @@ def gen(location, output,reference, quiet, verbose):
274274
@click.argument('location',
275275
required=True,
276276
metavar='LOCATION',
277+
callback=partial(validate_extensions, extensions=('.csv',)),
277278
type=click.Path(
278279
exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True))
279280

280281
@click.argument('output',
281282
required=True,
282-
callback=partial(validate_extensions, extensions=('.csv',)),
283283
metavar='OUTPUT',
284-
type=click.Path(exists=False, dir_okay=True, file_okay=False, writable=True, resolve_path=True))
284+
type=click.Path(exists=True, dir_okay=True, file_okay=False, writable=True, resolve_path=True))
285285

286286
@click.option('--api-key',
287287
metavar='API-KEY',
288+
envvar='DEJACODE_API_KEY',
288289
type=str,
289290
help='DejaCode License Library API KEY.')
290291

291292
@click.option('--api-url',
292293
metavar='API-URL',
294+
envvar='DEJACODE_API_URL',
293295
type=str,
294296
help='DejaCode License Library API URL.')
295297

@@ -321,7 +323,7 @@ def fetch_licenses(location, output, api_key, api_url, quiet, verbose): # NOQA
321323

322324
errors, abouts = gen.load_inventory(location)
323325

324-
licenses_by_key, fetch_errors = api.fetch_licenses(abouts, api_url, api_key)
326+
licenses_by_key, fetch_errors = api.fetch_licenses(abouts, api_url, api_key, verbose)
325327
errors.extend(fetch_errors)
326328

327329
for license in licenses_by_key.values(): # NOQA

src/attributecode/gen.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,12 @@ def load_inventory(location, base_dir=None):
8989
errors.append(Error(ERROR, msg))
9090
continue
9191
about_file_path = util.to_posix(about_file_path)
92-
92+
93+
# Ensure there is no absolute directory path
94+
about_file_path = about_file_path.strip('/')
95+
9396
segments = about_file_path.split('/')
94-
if any(seg !=seg.strip() for seg in segments):
97+
if any(seg != seg.strip() for seg in segments):
9598
msg = (
9699
'Invalid "about_file_path": must not end or start with a space. '
97100
'Cannot generate .ABOUT file for: "{}"'.format(about_file_path))
@@ -103,7 +106,6 @@ def load_inventory(location, base_dir=None):
103106
if base_dir:
104107
about.location = os.path.join(base_dir, about_file_path)
105108

106-
print('creating about:' + repr(about))
107109
abouts.append(about)
108110
except Exception as e:
109111
msg = (
@@ -216,29 +218,31 @@ def generate_about_files(inventory_location, target_dir,
216218
notices_by_name, extra_licenses_by_key = model.load_license_references(reference_dir)
217219
# we update (and possibly override) existing API-fetched licenses with local ones
218220
licenses_by_key.update(extra_licenses_by_key)
221+
222+
223+
# TODO: validate inventory!!!!! to catch error before creating ABOUT files
219224

220225
# update all licenses and notices
221226
for about in abouts:
227+
# fix the location to ensure this is a proper .ABOUT file
228+
loc = about.location
229+
if not loc.endswith('.ABOUT'):
230+
loc = loc.rstrip('\\/').strip() + '.ABOUT'
231+
about.location = loc
232+
222233
about.update_licenses(licenses_by_key)
223234

224235
if about.notice_file:
225236
notice_text = notices_by_name.get(about.notice_file)
226237
if not notice_text:
227238
msg = (
228-
'Empty or missing notice_file. '
229-
'Cannot generate valid .ABOUT file: {}'.format(about.notice_file))
239+
'Cannot generate valid .ABOUT file for: "{}". '
240+
'Empty or missing notice_file: {}'.format(about.location, about.notice_file))
230241
errors.append(Error(ERROR, msg))
231-
continue
232-
about.notice_text = notice_text
233-
234-
# TODO: validate inventory!!!!! to catch error begore creating ABOUT files
242+
else:
243+
about.notice_text = notice_text
235244

236-
# fix the location to ensure this is a proper .ABOUT file
237-
for about in abouts:
238-
loc = about.location
239-
if not loc.endswith('.ABOUT'):
240-
loc = loc.rstrip('\\/').strip() + '.ABOUT'
241-
about.location = loc
242-
about.dump(location=loc, with_files=True)
245+
# create the files proper
246+
about.dump(location=about.location, with_files=True)
243247

244248
return unique(errors), abouts

src/attributecode/model.py

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import re
3535
import traceback
3636

37+
import click
38+
3739
from attributecode.util import python2
3840

3941
if python2: # pragma: nocover
@@ -128,11 +130,12 @@ def from_dict(cls, data):
128130

129131
def dump(self, target_dir):
130132
"""
131-
Write this license as a .yml data file and a text file in `target_dir`.
133+
Write this license as a .yml data file and a .LICENSE text file in
134+
`target_dir`.
132135
"""
133136
data_loc = os.path.join(target_dir, self.key + '.yml')
134-
with io.open(data_loc, 'wb') as out:
135-
data_loc.write(saneyaml.dump(self.to_dict()))
137+
with io.open(data_loc, 'w', encoding='utf-8') as out:
138+
out.write(saneyaml.dump(self.to_dict()))
136139

137140
if self.text:
138141
file_loc = self.file_loc(target_dir)
@@ -145,12 +148,13 @@ def load_license_references(reference_dir):
145148
Return a mapping of notices as {notice_file: notice text} and a mapping of
146149
{license key: License} loaded from a `reference_dir`.
147150
In the `reference_dir`, all the files must be UTF-8 encoded text files:
148-
- The notice files MUST have a .NOTICE extension.
149151
- license files must be named after their license key and can consist of:
150152
- a text file with .LICENSE extension
151153
- an optional companion .yml YAML data file with extra license data to load
152154
as a License obejct.
153155
156+
- The notice files are any file that is not a license file pair.
157+
154158
For instance, we can have the files foo.LICENSE and foo.yml where foo.yml contains:
155159
156160
key: foo
@@ -159,41 +163,32 @@ def load_license_references(reference_dir):
159163
"""
160164
notices_by_name = {}
161165
licenses_by_key = {}
162-
163166
ref_files = os.listdir(reference_dir)
164-
for name in ref_files:
165-
loc = os.path.join(reference_dir, name)
166-
167-
if os.path.isdir(loc):
168-
# TODO: raise some error?
169-
continue
170-
171-
if name.endswith('.NOTICE'):
172-
with io.open(loc, encoding='utf-8') as inp:
173-
text = inp.read()
174-
notices_by_name[name] = text
175-
176-
177-
if name.endswith('.yml'):
178-
loaded = License.load(loc)
179-
180-
license_key = name.replace('.yml', '')
181-
lic = licenses_by_key.get(license_key)
182-
183-
if lic:
184-
lic.update(loaded)
185-
else:
186-
licenses_by_key[license_key] = loaded
187-
188-
if name.endswith('.LICENSE'):
189-
license_key = name.replace('.LICENSE', '')
190-
lic = licenses_by_key.get(license_key) or License(license_key)
191-
licenses_by_key[license_key] = lic
192-
193-
with io.open(loc, encoding='utf-8') as inp:
167+
data_files =[f for f in ref_files if f.endswith('.yml')]
168+
text_files = set([f for f in ref_files if not f.endswith('.yml')])
169+
170+
for data_file in data_files:
171+
loc = os.path.join(reference_dir, data_file)
172+
lic = License.load(loc)
173+
licenses_by_key[lic.key] = lic
174+
175+
if lic.file not in text_files:
176+
click.echo(
177+
'WARNING: The reference license: {} does not have a '
178+
'corresponding text file: {}'.format(lic.key, lic.file))
179+
else:
180+
with io.open(lic.file_loc(reference_dir), encoding='utf-8') as inp:
194181
text = inp.read()
195182
lic.text = text
196183

184+
text_files.remove(lic.file)
185+
186+
# whatever is left are "notice" files
187+
for notice_file in text_files:
188+
with io.open(loc, encoding='utf-8') as inp:
189+
text = inp.read()
190+
notices_by_name[notice_file] = text
191+
197192
return notices_by_name, licenses_by_key
198193

199194

tests/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def test_fetch_licenses(self, have_network_connection, valid_api_url):
9696
valid_api_url.return_value = False
9797
error_msg = (
9898
'Network problem. Please check your Internet connection. '
99-
'License generation is skipped.')
99+
'License retrieval is skipped.')
100100
expected = ({}, [Error(ERROR, error_msg)])
101101
assert api.fetch_licenses([], '', '') == expected
102102

0 commit comments

Comments
 (0)