From 6b0a516542d161163b49da31ff2d2c3886f62e3e Mon Sep 17 00:00:00 2001 From: Anne Schumacher Date: Thu, 3 Nov 2022 15:57:13 +0100 Subject: [PATCH 001/175] set new dev version number --- docs/docsify/_coverpage.md | 2 +- sparv/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docsify/_coverpage.md b/docs/docsify/_coverpage.md index 2a0aa5ee..aff138c4 100644 --- a/docs/docsify/_coverpage.md +++ b/docs/docsify/_coverpage.md @@ -4,7 +4,7 @@ > Språkbanken's text analysis tool -

version 5.1.0

+

version 5.1.1dev0

--> > -> Depending on your [notification settings](https://github.com/settings/notifications) you will now recieve notifications about new Sparv releases on GitHub's website, via email or on your phone. +> Depending on your [notification settings](https://github.com/settings/notifications) you will now receive notifications about new Sparv releases on GitHub's website, via email or on your phone. diff --git a/docs/user-manual/available-analyses.md b/docs/user-manual/available-analyses.md index c16107ae..1adcf2ac 100644 --- a/docs/user-manual/available-analyses.md +++ b/docs/user-manual/available-analyses.md @@ -10,7 +10,7 @@ whitespace information) are not listed here. > configuration](user-manual/corpus-configuration.md#export-options)). Please observe that the annotations usually have > shorter names in the corpus exports. > ->**Annotators** are the names of the annotation functions (including their module names) which are used for procuding +>**Annotators** are the names of the annotation functions (including their module names) which are used for producing >the annotations. They can be run directly with the `sparv run-rule [annotator]` command. In most cases this is not >necessary though, due to the fact that the annotation functions producing the annotations listed in the corpus config >file are executed automatically when running `sparv run`. @@ -121,7 +121,7 @@ preset](user-manual/corpus-configuration.md#annotation-presets) called `SWE_DEFA |:---|:-----------| |**Description** | SALDO IDs from the `:saldo.sense`-attribute are enriched with likelihoods. |**Tool** | [Sparv wsd](https://github.com/spraakbanken/sparv-wsd) -|**Dokumentation**| [Running the Koala word sense disambiguators](https://github.com/spraakbanken/sparv-wsd/blob/master/README.pdf) +|**Documentation**| [Running the Koala word sense disambiguators](https://github.com/spraakbanken/sparv-wsd/blob/master/README.pdf) |**Model** | - [ALL_512_128_w10_A2_140403_ctx1.bin](https://github.com/spraakbanken/sparv-wsd/blob/master/models/scouse/ALL_512_128_w10_A2_140403_ctx1.bin)
- [lem_cbow0_s512_w10_NEW2_ctx.bin](https://github.com/spraakbanken/sparv-wsd/blob/master/models/scouse/lem_cbow0_s512_w10_NEW2_ctx.bin) |**Annotations** | - `:wsd.sense` (identifies senses in SALDO along with their likelihoods) |**Annotators** | `wsd:annotate` @@ -188,7 +188,7 @@ for Swedish from the 1800's: |:---|:-----------| |**Description** | Sentence segments are analysed to enrich tokens with part-of-speech tags and morphosyntactic information. |**Tool** | [Hunpos](https://code.google.com/archive/p/hunpos/) -|**Model** | - [suc3_suc-tags_default-setting_utf8.model](https://github.com/spraakbanken/sparv-models/blob/master/hunpos/suc3_suc-tags_default-setting_utf8.model?raw=true) trained on [SUC 3.0](https://spraakbanken.gu.se/resurser/suc3)
- a word list along with the words' morphosyntactig information generated from the [Dalin morphology](https://spraakbanken.gu.se/resurser/dalinm) and the [Swedberg morphology](https://spraakbanken.gu.se/resurser/swedbergm) +|**Model** | - [suc3_suc-tags_default-setting_utf8.model](https://github.com/spraakbanken/sparv-models/blob/master/hunpos/suc3_suc-tags_default-setting_utf8.model?raw=true) trained on [SUC 3.0](https://spraakbanken.gu.se/resurser/suc3)
- a word list along with the words' morphosyntactic information generated from the [Dalin morphology](https://spraakbanken.gu.se/resurser/dalinm) and the [Swedberg morphology](https://spraakbanken.gu.se/resurser/swedbergm) |**Tagset** | [SUC MSD tags](https://spraakbanken.gu.se/korp/markup/msdtags.html) |**Annotations** | - `:hunpos.msd` (morphosyntactic tag)
- `:hunpos.pos` (part-of-speech tag |**Annotators** | - `hunpos:msdtag_hist`
- `hunpos:postag` diff --git a/docs/user-manual/corpus-configuration.md b/docs/user-manual/corpus-configuration.md index 88dfc050..4c94eac1 100644 --- a/docs/user-manual/corpus-configuration.md +++ b/docs/user-manual/corpus-configuration.md @@ -87,7 +87,7 @@ The `metadata` section of your corpus config contains metadata about your corpus The `import` section of your corpus config is used to give Sparv some information about your input files (i.e. your corpus). -- `import.source_dir` defines the location of your input files and it defaults to `source`. Sparv will check the +- `import.source_dir` defines the location of your input files, and it defaults to `source`. Sparv will check the source directory recursively for valid input files to process. - `import.importer` is used to tell Sparv which importer to use when processing your source files. The setting you diff --git a/sparv/api/util/tagsets/pos_to_upos.py b/sparv/api/util/tagsets/pos_to_upos.py index 7f2724e9..282388bb 100644 --- a/sparv/api/util/tagsets/pos_to_upos.py +++ b/sparv/api/util/tagsets/pos_to_upos.py @@ -1,4 +1,4 @@ -"""Map different POS (or MSD) tags to simple Universal Depenendy POS (UPOS) tags. +"""Map different POS (or MSD) tags to simple Universal Dependency POS (UPOS) tags. http://universaldependencies.org/u/pos/all.html """ @@ -27,7 +27,7 @@ def pos_to_upos(pos, lang, tagset): - """Map POS tags to Universal Depenendy POS tags.""" + """Map POS tags to Universal Dependency POS tags.""" if (lang, tagset) in CONVERTERS: lang_convert = CONVERTERS[(lang, tagset)] return lang_convert(pos) From 294145f31d653a4442542266d6fd2d3a9d79ceb6 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Wed, 18 Oct 2023 11:07:13 +0200 Subject: [PATCH 152/175] Show source file name also for unhandled errors --- sparv/core/run_snake.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sparv/core/run_snake.py b/sparv/core/run_snake.py index b552d3f8..5c8fa8df 100644 --- a/sparv/core/run_snake.py +++ b/sparv/core/run_snake.py @@ -139,7 +139,9 @@ def flush(): # something went wrong. exit_with_error_message(e.message, "sparv.modules." + module_name) except Exception as e: - errmsg = f"An error occurred while executing {module_name}:{f_name}:\n\n {type(e).__name__}: {e}" + current_file = f" for the file {snakemake.params.source_file!r}" if snakemake.params.source_file else "" + errmsg = f"An error occurred while executing {module_name}:{f_name}{current_file}:" \ + f"\n\n {type(e).__name__}: {e}" if logger.level > logging.DEBUG: errmsg += f"\n\nTo display further details when errors occur, run Sparv with the '--log debug' or " \ "'--log-to-file debug' arguments." From a4275d77da42d8a16c6e7dfa07cb2c5f5bcdbb0f Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Wed, 18 Oct 2023 11:27:22 +0200 Subject: [PATCH 153/175] Raise error when dateformat.pre_regex doesn't match --- sparv/modules/dateformat/dateformat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sparv/modules/dateformat/dateformat.py b/sparv/modules/dateformat/dateformat.py index b7fb831f..aa44e5d2 100644 --- a/sparv/modules/dateformat/dateformat.py +++ b/sparv/modules/dateformat/dateformat.py @@ -228,6 +228,8 @@ def get_date_length(informat): if pre_regex: matches = re.match(pre_regex, val) + if not matches: + raise SparvErrorMessage(f"dateformat.pre_regex did not match the value {val!r}") val = [v for v in matches.groups() if v][0] if not val: # If the regex doesn't match, treat as no date From 1b1ecc585ef6a4e13419011291de4b6121d8b8b9 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Tue, 24 Oct 2023 12:00:56 +0200 Subject: [PATCH 154/175] Don't handle localhost differently during installations User may want to actually connect to localhost --- sparv/api/util/install.py | 3 +-- sparv/modules/korp/config.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sparv/api/util/install.py b/sparv/api/util/install.py index b1882b31..8c68c2ac 100644 --- a/sparv/api/util/install.py +++ b/sparv/api/util/install.py @@ -34,7 +34,6 @@ def install_mysql(host: Optional[str], db_name: str, sqlfile: Union[str, List[st db_name: Name of the database. sqlfile: Path to a SQL file, or list of paths. """ - local = host is None or host == "localhost" if isinstance(sqlfile, str): sqlfile = [sqlfile] @@ -49,7 +48,7 @@ def install_mysql(host: Optional[str], db_name: str, sqlfile: Union[str, List[st logger.info("Skipping empty file: %s (%d/%d)", f, file_count, file_total) else: logger.info(f"Installing MySQL database: {db_name}, source: {f} ({file_count}/{file_total})") - if local: + if not host: subprocess.check_call( f"cat {shlex.quote(f)} | mysql {shlex.quote(db_name)}", shell=True ) diff --git a/sparv/modules/korp/config.py b/sparv/modules/korp/config.py index a2f8a410..830f84b3 100644 --- a/sparv/modules/korp/config.py +++ b/sparv/modules/korp/config.py @@ -392,7 +392,7 @@ def build_annotations( def get_presets(remote_host, config_dir): """Get list of presets from file system.""" presets = {} - if remote_host and remote_host != "localhost": + if remote_host: remote_path = shlex.quote(f"{config_dir}/attributes/") cmd = ["ssh", remote_host, f"find {remote_path}"] else: From 3f241a05129432d9355c69111fee78ebea7f4231 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Fri, 27 Oct 2023 15:20:26 +0200 Subject: [PATCH 155/175] Don't automatically add plain annotations when all connected attributes are omitted --- sparv/core/misc.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/sparv/core/misc.py b/sparv/core/misc.py index be510940..bf04a4ed 100644 --- a/sparv/core/misc.py +++ b/sparv/core/misc.py @@ -62,7 +62,7 @@ def parse_annotation_list(annotation_names: Optional[Iterable[str]], all_annotat possible_plain_annotations = set() omit_annotations = set() include_rest = False - plain_to_atts = defaultdict(list) + plain_to_atts = defaultdict(set) result: OrderedDict = OrderedDict() for a in annotation_names: @@ -77,11 +77,11 @@ def parse_annotation_list(annotation_names: Optional[Iterable[str]], all_annotat plain_name, attr = Annotation(name).split() else: plain_name, attr = None, None - result.pop(name, None) + result.pop(name, None) # Remove any previous occurrence first, to keep the order result[name] = export_name or None if attr: possible_plain_annotations.add(plain_name) - plain_to_atts[plain_name].append(name) + plain_to_atts[plain_name].add(name) else: plain_annotations.add(name) @@ -91,17 +91,25 @@ def parse_annotation_list(annotation_names: Optional[Iterable[str]], all_annotat # Add all_annotations to result if required if include_rest and all_annotations: - for a in [a for a in all_annotations if not a in omit_annotations]: - if a not in result: + for a in all_annotations: + if a not in result and a not in omit_annotations: result[a] = None - plain_name, _ = Annotation(a).split() - plain_to_atts[plain_name].append(a) + plain_name, attr = Annotation(a).split() + if attr: + plain_to_atts[plain_name].add(a) plain_annotations.add(plain_name) # Add annotations names without attributes to result if required if add_plain_annotations: - for a in sorted(possible_plain_annotations.difference(plain_annotations)): - if a not in result: + new_plain_annotations = possible_plain_annotations.difference(plain_annotations) + if omit_annotations: + # Don't add new plain annotation if all connected attributes have been omitted + for annotation in omit_annotations: + plain_name, _ = Annotation(annotation).split() + plain_to_atts[plain_name].discard(annotation) + + for a in sorted(new_plain_annotations): + if a not in result and plain_to_atts[a]: result[a] = None # Remove any exclusions from final list From 256b1bf8a0fc07a16afba630227d06c1dc06d708 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Fri, 27 Oct 2023 17:10:10 +0200 Subject: [PATCH 156/175] =?UTF-8?q?Remove=20single=20comma=20that=20preven?= =?UTF-8?q?ted=20Stanza=20from=20using=20GPU=20=F0=9F=98=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sparv/modules/stanza/stanza_swe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sparv/modules/stanza/stanza_swe.py b/sparv/modules/stanza/stanza_swe.py index 51e94b9e..8aa08c1a 100644 --- a/sparv/modules/stanza/stanza_swe.py +++ b/sparv/modules/stanza/stanza_swe.py @@ -133,7 +133,7 @@ def annotate_swe( logger.debug(f"Running dependency parsing and POS-taggning on {len(sentences)} sentences" f" (using {'GPU' if use_gpu and not fallback else 'CPU'}).") nlp_args["processors"] = "tokenize,pos,lemma,depparse" # Comma-separated list of processors to use - nlp_args["use_gpu"] = use_gpu and not fallback, + nlp_args["use_gpu"] = use_gpu and not fallback nlp = stanza.Pipeline(**nlp_args) else: From 7f8c804f29d8ac41550d4eae840517cd8335164e Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Fri, 27 Oct 2023 17:19:41 +0200 Subject: [PATCH 157/175] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e96eb169..9a68c1d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,8 @@ - Illegal characters are now replaced with underscore in XML element and attribute names during XML export. This also applies to CWB and Korp config exports. - Not specifying a corpus language now excludes all language specific annotators. +- When an unhandled exception occurs, the relevant source document will be displayed in the log. +- `localhost` as an installation target is no longer handled as if host was omitted. ### Fixed @@ -69,6 +71,7 @@ - Elapsed time exceeding 24 hours no longer gets cut off in the `--stats` output. - Fixed bug where error messages were not getting written to the log file when the `--log debug` flag was used. +- Fixed bug that prevented Stanza from using GPU. ## [5.1.0] - 2022-11-03 From 0aa0828b35f205e4b548d07a8117ec36564918de Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Mon, 30 Oct 2023 15:35:26 +0100 Subject: [PATCH 158/175] Use lazy formatting in some logging --- sparv/api/util/export.py | 18 +++++++++---- sparv/api/util/install.py | 2 +- sparv/api/util/system.py | 4 +-- sparv/core/config.py | 2 +- sparv/core/io.py | 20 ++++++++++----- sparv/modules/cwb/install_corpus.py | 4 +-- sparv/modules/hist/models.py | 2 +- sparv/modules/korp/config.py | 18 ++++++++++--- sparv/modules/misc/number.py | 4 +-- .../phrase_structure/phrase_structure.py | 25 ++++++++++++++++--- sparv/modules/stanza/models.py | 2 +- sparv/modules/stanza/stanza_swe.py | 9 ++++--- sparv/modules/stats_export/sbx_stats.py | 4 +-- sparv/modules/stats_export/stats_export.py | 2 +- 14 files changed, 81 insertions(+), 35 deletions(-) diff --git a/sparv/api/util/export.py b/sparv/api/util/export.py index c38c7bad..bee5632a 100644 --- a/sparv/api/util/export.py +++ b/sparv/api/util/export.py @@ -515,15 +515,23 @@ def _check_name_collision(export_names, source_annotations): source_annot = annots[0] if annots[0].name in source_names else annots[1] new_name = SPARV_DEFAULT_NAMESPACE + "." + export_names[sparv_annot.name] export_names[sparv_annot.name] = new_name - logger.info("Changing name of automatic annotation '{}' to '{}' due to collision with '{}'.".format( - sparv_annot.name, new_name, source_annot.name)) + logger.info( + "Changing name of automatic annotation '%s' to '%s' due to collision with '%s'.", + sparv_annot.name, + new_name, + source_annot.name + ) # Warn the user if we cannot resolve collisions automatically else: annots_string = "\n".join([f"{a.name} ({'source' if a.name in source_names else 'sparv'} annotation)" for a in annots]) - logger.warning("The following annotations are exported with the same name ({}) and might overwrite " - "each other: \n\n{}\n\nIf you want to keep all of these annotations you can change " - "their export names.".format(attr, annots_string)) + logger.warning( + "The following annotations are exported with the same name (%s) and might overwrite " + "each other: \n\n%s\n\nIf you want to keep all of these annotations you can change " + "their export names.", + attr, + annots_string + ) return export_names ################################################################################ diff --git a/sparv/api/util/install.py b/sparv/api/util/install.py index 8c68c2ac..06d5e16a 100644 --- a/sparv/api/util/install.py +++ b/sparv/api/util/install.py @@ -47,7 +47,7 @@ def install_mysql(host: Optional[str], db_name: str, sqlfile: Union[str, List[st elif os.path.getsize(f) < 10: logger.info("Skipping empty file: %s (%d/%d)", f, file_count, file_total) else: - logger.info(f"Installing MySQL database: {db_name}, source: {f} ({file_count}/{file_total})") + logger.info("Installing MySQL database: %s, source: %s (%d/%d)", db_name, f, file_count, file_total) if not host: subprocess.check_call( f"cat {shlex.quote(f)} | mysql {shlex.quote(db_name)}", shell=True diff --git a/sparv/api/util/system.py b/sparv/api/util/system.py index 7ad75c93..758f3d75 100644 --- a/sparv/api/util/system.py +++ b/sparv/api/util/system.py @@ -159,10 +159,10 @@ def rsync(local: Union[str, Path], host: Optional[str], remote: Union[str, Path] remote_dir = os.path.dirname(remote) if os.path.isdir(local): - logger.info(f"Copying directory: {local} => {host + ':' if host else ''}{remote}") + logger.info("Copying directory: %s => %s%s", local, host + ":" if host else "", remote) args = ["--recursive", "--delete", f"{local}/"] else: - logger.info(f"Copying file: {local} => {host + ':' if host else ''}{remote}") + logger.info("Copying file: %s => %s%s", local, host + ":" if host else "", remote) args = [local] if host: diff --git a/sparv/core/config.py b/sparv/core/config.py index e494f846..974ef4bb 100644 --- a/sparv/core/config.py +++ b/sparv/core/config.py @@ -78,7 +78,7 @@ def load_config(config_file: Optional[str], config_dict: Optional[dict] = None) if DEFAULT_CONFIG.is_file(): _config_default = read_yaml(DEFAULT_CONFIG) else: - logger.warning("Default config file is missing: {}".format(DEFAULT_CONFIG)) + logger.warning("Default config file is missing: %s", DEFAULT_CONFIG) _config_default = {} if config_file: diff --git a/sparv/core/io.py b/sparv/core/io.py index 0dc36146..167e3100 100644 --- a/sparv/core/io.py +++ b/sparv/core/io.py @@ -111,7 +111,7 @@ def _write_single_annotation(source_file: str, annotation: str, values, append: ctr += 1 # Update file modification time even if nothing was written os.utime(file_path, None) - logger.info(f"Wrote {ctr} items: {source_file + '/' if source_file else ''}{annotation}") + logger.info("Wrote %d items: %s%s", ctr, source_file + "/" if source_file else "", annotation) def get_annotation_size(source_file: str, annotation: BaseAnnotation): @@ -215,7 +215,7 @@ def _read_single_annotation(source_file: str, annotation: str, with_annotation_n if isinstance(e, OSError) and str(e) != "Invalid data stream": raise e raise_format_error(ann_file) - logger.debug(f"Read {ctr} items: {source_file + '/' if source_file else ''}{annotation}") + logger.debug("Read %d items: %s%s", ctr, source_file + "/" if source_file else "", annotation) def write_data(source_file: Optional[str], name: Union[BaseAnnotation, str], value: str, append: bool = False): @@ -228,8 +228,12 @@ def write_data(source_file: Optional[str], name: Union[BaseAnnotation, str], val f.write(value) # Update file modification time even if nothing was written os.utime(file_path, None) - logger.info(f"Wrote {len(value)} bytes: {source_file + '/' if source_file else ''}" - f"{name.name if isinstance(name, BaseAnnotation) else name}") + logger.info( + "Wrote %d bytes: %s%s", + len(value), + source_file + "/" if source_file else "", + name.name if isinstance(name, BaseAnnotation) else name + ) def read_data(source_file: Optional[str], name: Union[BaseAnnotation, str]): @@ -244,8 +248,12 @@ def read_data(source_file: Optional[str], name: Union[BaseAnnotation, str]): raise e raise_format_error(file_path) - logger.debug(f"Read {len(data)} bytes: {source_file + '/' if source_file else ''}" - f"{name.name if isinstance(name, BaseAnnotation) else name}") + logger.debug( + "Read %d bytes: %s%s", + len(data), + source_file + "/" if source_file else "", + name.name if isinstance(name, BaseAnnotation) else name + ) return data diff --git a/sparv/modules/cwb/install_corpus.py b/sparv/modules/cwb/install_corpus.py index e97f5579..d9cc9bdc 100644 --- a/sparv/modules/cwb/install_corpus.py +++ b/sparv/modules/cwb/install_corpus.py @@ -70,11 +70,11 @@ def uninstall_corpus( assert corpus and data_dir and registry_dir # Already checked by Sparv, but just to be sure registry_file = Path(registry_dir) / corpus - logger.info(f"Removing CWB registry file from {host + ':' if host else ''}{registry_file}") + logger.info("Removing CWB registry file from %s%s", host + ":" if host else "", registry_file) util.install.uninstall_path(registry_file, host=host) corpus_dir = Path(data_dir) / corpus - logger.info(f"Removing CWB data from {host + ':' if host else ''}{corpus_dir}") + logger.info("Removing CWB data from %s%s", host + ":" if host else "", corpus_dir) util.install.uninstall_path(corpus_dir, host=host) install_marker.remove() diff --git a/sparv/modules/hist/models.py b/sparv/modules/hist/models.py index 3a99d891..69e3ece6 100644 --- a/sparv/modules/hist/models.py +++ b/sparv/modules/hist/models.py @@ -166,7 +166,7 @@ def read_lmf(xml, annotation_elements=("writtenForm", "lemgram"), verbose=True, "katt", "doktor"] util.misc.test_lexicon(lexicon, testwords) - logger.info(f"OK, read {len(lexicon)} entries") + logger.info("OK, read %d entries", len(lexicon)) return lexicon diff --git a/sparv/modules/korp/config.py b/sparv/modules/korp/config.py index 830f84b3..b4d54578 100644 --- a/sparv/modules/korp/config.py +++ b/sparv/modules/korp/config.py @@ -342,7 +342,7 @@ def build_annotations( and annotation.name not in annotation_definitions and not export_name in include ): - logger.debug(f"Skipping annotation {annotation.name!r}") + logger.debug("Skipping annotation '%s'", annotation.name) continue export_name_cwb = cwb_escape(export_name.replace(":", "_")) is_token = annotation.annotation_name == token.name @@ -397,7 +397,11 @@ def get_presets(remote_host, config_dir): cmd = ["ssh", remote_host, f"find {remote_path}"] else: cmd = ["find", f"{config_dir}/attributes/"] - logger.debug(f"Getting Korp annotation presets from {remote_host}:{config_dir}") + logger.debug( + "Getting Korp annotation presets from %s%s", + remote_host + ":" if remote_host else "", + config_dir + ) s = subprocess.run(cmd, capture_output=True, encoding="utf-8") if s.returncode == 0: for p in s.stdout.splitlines(): @@ -451,7 +455,11 @@ def install_config( ): """Install Korp corpus configuration file.""" corpus_dir = Path(config_dir) / "corpora" - logger.info(f"Installing Korp corpus configuration file to {remote_host}:{corpus_dir}") + logger.info( + "Installing Korp corpus configuration file to %s%s", + remote_host + ":" if remote_host else "", + corpus_dir + ) util.install.install_path(config_file, remote_host, corpus_dir) uninstall_marker.remove() marker.write() @@ -468,7 +476,9 @@ def uninstall_config( """Uninstall Korp corpus configuration file.""" corpus_file = Path(config_dir) / "corpora" / f"{corpus_id}.yaml" logger.info( - f"Uninstalling Korp corpus configuration file from {remote_host + ':' if remote_host else ''}{corpus_file}" + "Uninstalling Korp corpus configuration file from %s%s", + remote_host + ":" if remote_host else "", + corpus_file ) util.install.uninstall_path(corpus_file, host=remote_host) install_marker.remove() diff --git a/sparv/modules/misc/number.py b/sparv/modules/misc/number.py index 540ed5db..6bb40552 100644 --- a/sparv/modules/misc/number.py +++ b/sparv/modules/misc/number.py @@ -152,7 +152,7 @@ def count_chunks(out: OutputCommonData = OutputCommonData("misc.{annotation}_cou pass if chunk_count == 0: - logger.info(f"No {chunk.name} chunks found in corpus") + logger.info("No %s chunks found in corpus", chunk.name) # Write chunk count data out.write(str(chunk_count)) @@ -163,7 +163,7 @@ def count_chunks(out: OutputCommonData = OutputCommonData("misc.{annotation}_cou def count_zero_chunks(out: OutputCommonData = OutputCommonData("misc.{annotation}_count"), _files: AllSourceFilenames = AllSourceFilenames()): """Create chunk count file for non-existent 'annotation' chunks.""" - logger.info(f"No {out.name[5:-6]} chunks found in corpus") + logger.info("No %s chunks found in corpus", out.name[5:-6]) out.write("0") diff --git a/sparv/modules/phrase_structure/phrase_structure.py b/sparv/modules/phrase_structure/phrase_structure.py index 5e8b291c..5325cc73 100644 --- a/sparv/modules/phrase_structure/phrase_structure.py +++ b/sparv/modules/phrase_structure/phrase_structure.py @@ -54,24 +54,41 @@ def get_token_span(index): if not child[0].startswith("WORD:"): start_pos = get_token_span(s[position])[0] open_elem_stack.append(child + (start_pos,)) - logger.debug(f" {s[position]}") + logger.debug( + " %s", + child[0], + child[1], + s[position] + ) else: # Close nodes while open_elem_stack[-1][2] == child[2]: start_pos = open_elem_stack[-1][3] end_pos = get_token_span(s[position - 1])[1] nodes.append(((start_pos, end_pos), open_elem_stack[-1][0], open_elem_stack[-1][1])) - logger.debug(f" {start_pos}-{end_pos}") + logger.debug( + " %d-%d", + open_elem_stack[-1][0], + open_elem_stack[-1][1], + start_pos, + end_pos + ) open_elem_stack.pop() position += 1 - logger.debug(f" {child[0][5:]}") + logger.debug(" %s", child[0][5:]) # Close remaining open nodes end_pos = get_token_span(s[-1])[1] for elem in reversed(open_elem_stack): start_pos = elem[3] nodes.append(((start_pos, end_pos), elem[0], elem[1])) - logger.debug(f" {start_pos}-{end_pos}") + logger.debug( + " %d-%d", + elem[0], + elem[1], + start_pos, + end_pos + ) # Sort nodes sorted_nodes = sorted(nodes) diff --git a/sparv/modules/stanza/models.py b/sparv/modules/stanza/models.py index 2270f27f..96cf4fb2 100644 --- a/sparv/modules/stanza/models.py +++ b/sparv/modules/stanza/models.py @@ -70,7 +70,7 @@ def get_model(lang: Language = Language(), import stanza lang_name = util.misc.get_language_name_by_part3(lang) or lang stanza_lang = util.misc.get_language_part1_by_part3(lang) - logger.info(f"Downloading Stanza language model for {lang_name}") + logger.info("Downloading Stanza language model for %s", lang_name) stanza.download(lang=stanza_lang, model_dir=str(resources_file.path.parent), verbose=False, logging_level=logging.WARNING) zip_file = Model(f"stanza/{lang}/{stanza_lang}/default.zip") diff --git a/sparv/modules/stanza/stanza_swe.py b/sparv/modules/stanza/stanza_swe.py index 8aa08c1a..0e171219 100644 --- a/sparv/modules/stanza/stanza_swe.py +++ b/sparv/modules/stanza/stanza_swe.py @@ -130,14 +130,17 @@ def annotate_swe( # Init Stanza pipeline if dep or fallback: - logger.debug(f"Running dependency parsing and POS-taggning on {len(sentences)} sentences" - f" (using {'GPU' if use_gpu and not fallback else 'CPU'}).") + logger.debug( + "Running dependency parsing and POS-taggning on %d sentences (using %s).", + len(sentences), + "GPU" if use_gpu and not fallback else "CPU" + ) nlp_args["processors"] = "tokenize,pos,lemma,depparse" # Comma-separated list of processors to use nlp_args["use_gpu"] = use_gpu and not fallback nlp = stanza.Pipeline(**nlp_args) else: - logger.debug(f"Running POS-taggning on {len(sentences)} sentences.") + logger.debug("Running POS-taggning on %d sentences.", len(sentences)) nlp_args["processors"] = "tokenize,pos" # Comma-separated list of processors to use nlp_args["use_gpu"] = use_gpu nlp = stanza.Pipeline(**nlp_args) diff --git a/sparv/modules/stats_export/sbx_stats.py b/sparv/modules/stats_export/sbx_stats.py index 4689bf84..8eb36478 100644 --- a/sparv/modules/stats_export/sbx_stats.py +++ b/sparv/modules/stats_export/sbx_stats.py @@ -264,7 +264,7 @@ def uninstall_sbx_freq_list( ): """Uninstall SBX word frequency list.""" remote_file = os.path.join(remote_dir, f"stats_{corpus_id}.csv") - logger.info(f"Removing SBX word frequency file {host + ':' if host else ''}{remote_file}") + logger.info("Removing SBX word frequency file %s%s", host + ":" if host else "", remote_file) util.install.uninstall_path(remote_file, host) install_marker.remove() marker.write() @@ -280,7 +280,7 @@ def uninstall_sbx_freq_list_date( ): """Uninstall SBX word frequency list with dates.""" remote_file = os.path.join(remote_dir, f"stats_{corpus_id}.csv") - logger.info(f"Removing SBX word frequency with dates file {host + ':' if host else ''}{remote_file}") + logger.info("Removing SBX word frequency with dates file %s%s", host + ":" if host else "", remote_file) util.install.uninstall_path(remote_file, host) install_marker.remove() marker.write() diff --git a/sparv/modules/stats_export/stats_export.py b/sparv/modules/stats_export/stats_export.py index cf6e9eb1..ab4c0ec3 100644 --- a/sparv/modules/stats_export/stats_export.py +++ b/sparv/modules/stats_export/stats_export.py @@ -120,7 +120,7 @@ def uninstall_freq_list( ): """Uninstall word frequency list.""" remote_file = os.path.join(remote_dir, f"stats_{corpus_id}.csv") - logger.info(f"Removing word frequency file {host + ':' if host else ''}{remote_file}") + logger.info("Removing word frequency file %s%s", host + ":" if host else "", remote_file) util.install.uninstall_path(remote_file, host) install_marker.remove() marker.write() From 49585e576f67b0d21b41fd8fe641b5acd836fdfb Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Mon, 6 Nov 2023 16:06:41 +0100 Subject: [PATCH 159/175] Extend run-rule autocompletion to include everything --- sparv/__main__.py | 4 ++++ sparv/core/Snakefile | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sparv/__main__.py b/sparv/__main__.py index 6b043008..8bfe3b3b 100644 --- a/sparv/__main__.py +++ b/sparv/__main__.py @@ -87,6 +87,10 @@ def __call__(self, parsed_args, **kwargs): except EOFError: # Cache placeholder created but not yet populated pass + # run-rule includes everything + if self.type == "annotate": + return [v for t in cache_data.values() for v in t] + return cache_data.get(self.type, []) diff --git a/sparv/core/Snakefile b/sparv/core/Snakefile index b57624a8..e24565d3 100644 --- a/sparv/core/Snakefile +++ b/sparv/core/Snakefile @@ -154,14 +154,14 @@ def update_autocompletion_cache(): import pickle # Collect data for cache - new_cache = dict.fromkeys(("import", "export", "install", "uninstall", "model")) + new_cache = dict.fromkeys(("export", "install", "uninstall", "model")) for target_type in new_cache: new_cache[target_type] = sorted( [t[0] for t in getattr(snake_storage, f"{target_type}_targets") if not t[0].startswith("custom.")] ) new_cache["annotate"] = [] - for target_type in ("named", "custom"): + for target_type in ("named", "custom", "import"): new_cache["annotate"].extend( [t[0] for t in getattr(snake_storage, f"{target_type}_targets") if not t[0].startswith("custom.")] ) From 67f2a9dcf577fabd96de0d6845fe6c2237addf54 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Mon, 6 Nov 2023 16:09:28 +0100 Subject: [PATCH 160/175] Fix crash when exporting scrambled XML without any text --- CHANGELOG.md | 1 + sparv/modules/xml_export/scrambled.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a68c1d2..c646dd07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ - Fixed bug where error messages were not getting written to the log file when the `--log debug` flag was used. - Fixed bug that prevented Stanza from using GPU. +- Fixed crash when exporting scrambled XML without any text. ## [5.1.0] - 2022-11-03 diff --git a/sparv/modules/xml_export/scrambled.py b/sparv/modules/xml_export/scrambled.py index df2d68f6..c2c4ba99 100644 --- a/sparv/modules/xml_export/scrambled.py +++ b/sparv/modules/xml_export/scrambled.py @@ -73,6 +73,14 @@ def scrambled(source_file: SourceFilename = SourceFilename(), # Reorder chunks new_span_positions = util.export.scramble_spans(span_positions, chunk.name, chunk_order) + # If the scrambled document contains no text, export a document containing just the root node and nothing else (we + # need to produce a file, and an empty file would be invalid XML). + # Alternatively, we could export the original (span_positions), but then any text outside the scramble_on chunks + # would be included, unscrambled, and we don't want to risk that. + if not new_span_positions: + logger.warning(f"{source_file!r} contains no text after scrambling (using the annotation {chunk.name!r})") + new_span_positions = [span_positions[0], span_positions[-1]] + # Construct XML string xmlstr = xml_utils.make_pretty_xml(new_span_positions, annotation_dict, export_names, token.name, word_annotation, fileid_annotation, include_empty_attributes, sparv_namespace, xml_namespaces) From 254b4d3dddac7f4d38b932c128647f1282bbb2c9 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Thu, 9 Nov 2023 11:37:43 +0100 Subject: [PATCH 161/175] Change to the new logo --- docs/docsify/_coverpage.md | 2 +- docs/docsify/index.html | 2 +- docs/images/sparv.png | Bin 59040 -> 0 bytes docs/images/sparv_detailed.png | Bin 0 -> 9243 bytes docs/md2pdf/make_pdf.sh | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 docs/images/sparv.png create mode 100644 docs/images/sparv_detailed.png diff --git a/docs/docsify/_coverpage.md b/docs/docsify/_coverpage.md index aff138c4..2d235052 100644 --- a/docs/docsify/_coverpage.md +++ b/docs/docsify/_coverpage.md @@ -1,4 +1,4 @@ - + # Sparv Pipeline Documentation diff --git a/docs/docsify/index.html b/docs/docsify/index.html index 87a2ac5c..c9fddc44 100644 --- a/docs/docsify/index.html +++ b/docs/docsify/index.html @@ -9,7 +9,7 @@ name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" /> - + 98=JWr84xMfWOf0zrqKfIpuY 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

!83nQ z@>*HVD_y7LHseqWm6PWl@MFkZBh6IG(|CguZZi4(yZw2j?z9?)f_k?`esMur^muR( z@DxCDken>b28gO15D2(u-6PMixqcSDc5)VzzlXOsm}4HfH5Zi)ut)Hpo&W`=*kL*s zwQD>p)wR&zRZze;wj)ro=xs9S@qW@_4ll_EWaM|-rZA}qOi8@y5ST78>5Geq;Jo?d z*`fu9TS2&oR8^@aZen})KZ()RW;0ser+9R4XsQ^NQSQ(0HvhV)gU);-1{%aDfW?4N97gRvOuK(l46EP;3)MuZySNU2Yq@0 zPAey09#MJ$sbW-{KQ3)`DF3K!6SNkSYZSh-$$bfNJxR|3h&Bs;NxbR<$2Tn47ga0yC>^o#d1!b`yBF%aPH5B&mOsm zCIFKIh3p<42!UpK!iqs@TFK7O{}EHtkxp$%@i9BhOzT?8tp#e5#t&L+jkH3lxzA`J zj2Kh_tFo-4LSKL`ONjhbjnnZ0TZ-{~S>sM$c=20v@`aOqKfpQRI9C4( zJBEFZ>~g~!>>>`Eo0~zk9`WA`r|#H>bh9`V7CTtwBnNx~DthsYfV_ZeRB!ZF@Q9_Z zGj}i;ACjrCjcdSdHaNhr*|!Ha`Lh+ixh<*G0nCNq8Bjm_h`L}OwSXY-$5W%@4YvRG7D>*mXMt{WBI^bl>WQzI19z?cjhR9)z< z3&xv!w_2z0Hk2x)Z)W8Mv=T73#{nWL_{%yk$Bq)Xy?I}?3wcRed6Z#;zM{toG}o#? z*N$1nd+?swwWMhvx+embjesdBdWcQ&yD1wwIHec;bHr5$$a`NvWA--nBIW4ss0Bx^9y?#ma#s6~Wx zXk@M`TQ^Z5y`82G-i&bF5~ttrdM^2HiRB3F9gfZ+F z+`Q2GG3IMrngozykW;gVhfmMR%S;T9?PEbpLWYHaS z96|UpEOfIB-DV^7Vc-a{cpXTq&XxB@>Lw1y{x$qp0M4^3B`T}p*9$D8Rw8>ki2fj} zqKNAYcU$QOld%5!V+9>`8G-S}HK6FgoXo@*`o+ubCm@h0H2sHJ7?J(-BvY!ygYQr& zj11JFG*bUeZg>f1=wC~UDp+nq{_nvPHGeb1W1h+ipWlU-p_ZG0B40&!0gT_c#0DvH zkBPQjyPPvB*-$??3yK>92y0?dV`-Z%gd+L#jATmejb*=4NvPsG3sSMRkW#Hd`ZN=cq13 zNvI%LwoR25=Rp~;WolyTSc^y~13e)`2WWD1%AIh&DF`SG87~|F)wcFgO&NkJO6JpT zUd8!OJs_!0i;6^3#4EXbd*l6}IqcB!vz^ls^KerNAQ~ozjcbvS<{|np1>#lx;e_zv zS}@3GWB(~br$qLEHD{NPr;RtBBVRN7gX459WGXh=4LEEHiLbOG;$%LOUo^ZYJhX2o zXB#|Vam(gkY|3Q~4gP_3Pe?zx9WsR!yVZ%kpH$GE;l-9`{W(>SSv{v4p6gVL&?47r zdnF_Qvw_{^$AI-M(>eH%9XeU-YW2AA-sy{+D^d1}_gq6Dah-Iu@5n+a9aAZ$1d(GP zI8fWyWKlo=_WjxP38UgeHz}&iWt_;(gI4(3*2H_X!pVA zRnN-Nu?drIZ1%W+YrYp#3MxNZlIQ*c+qHV#P9G1k4qxDkx3U30&0H?^oJBQ)Rjz`Z;?19#fsCv$1UB|Q{; zwwwCy?gjjecQypVeBLE?rX!cyfeBt}%HGJNhFKwRq7+ssN$F09!qE0I%BQuG>$nNlD|$HJ=wt-GqozSr)X=aqOfERby7g62rLFCS(&*OSu#w))Gq8rxy0 zHwf}N0K2YIV748h7)JTl(IoGGGoy0Q6mjm^>+2gQOc2Rtp7;N+W2KK7)ONbs2(+fQ ztZ`{5eMaQ03hFq5EGfm}E#;RL;3d@=d*5sQs0iyY7?=Yp(P3n;C&1SzR}#SFp{_n4 zm9-w5L`bESwO;;I$eBi@tbSJ;6#kK>$0Ofac^kQ7mNr8gC1h7m#H5O zzY^?vljO^n7RcEpuEVf-DT<&aA(*kRy|1VV-^I*gcl*s`BCZkLtnZ0&(&>QM>^$7`Vlu?JSwdJ$tN~2;v5F$P z`6VOo@Ov4QJcYOeD+3+@7tC3BTd4DFLqUzI8=-|J*r&oWx4J+e zdnW?Dm-5i)#DB9{s;RPKyS!FwVi1!cOSbeirE|?SZioJc@F~fL%%kRwKnv6pM5A|% z-u^7Wo3@xEFr7o+{XD_5{N&!7BM(N8m4$yT$Uf8Ww+?E@xZHcEWrxy6Wb%7GIB<(P zpQsD;=YaguiE)=j7#f(t)x*zt-U@`KMWpp&lm^~;%Z}~*1$(W02)XWxfX4UF$yiFD z`0z8cC0HR?!9lCHFM%|fZU{W~Oer_!N-_0eNu~vygBzuxfG219!2Oaq!u_tEmuz7%n)-rV(EGYZvGs=ivqf^OB4qFJf|k4&FSab?5gp=*+%2J ztfFE!4MKeQf@vUc2LkUP0j-_XF!ey^MCSVm#zD`M|FUv0Yu0KsGfkz);dpMl*z z{jL>nXq%q^6D8)V^rOw?W*0PlmOGg5CD_FYt%;nZ;7TCa*&pPHI6 z^rL+Pq8slb;`R2)A%r_p-UC41jJtREbru?r8_eKvkva1TQJ8qZltS`WHJ%lLNZbx8 zyEXX78ck1)lNKya|Em7mrG?*l;>#QtC?S3(OA?s)Rc>T}jm541USGdE>3HxURMw(U zbT`cPJr|N#yYFAw#gh%Y$Fq*ruQ1??(dq7k5)h^r9(KXzAYlll^a`JZ&7Z~r*(JbA zOUr|rWzPB9`nq539@`@jPT1 z1;Xar$Qp)SxRC!GsFyu? z<0A!|myb+fx?FrwtUv;a+2mN3!)%1mY`|=}LAi2=@#E25tgz~CU7|VlS5(=)VaONA z3zGVB6h+k}EnYP~8os%D=ma)d z8DRw{vizWL47Y=c!gnGa1Qp()wck1PJ+o>=?hs3(?rFmA*>xi+ zsd8xh9a(|OX2O6=U$@MGU}ufiv|Fzt$Bwd1+mEy7RYX}Fy2{lTot&-303 z={ztQQx-(ik(bKYx|AM&38}~hyau43<<%%JE?$eQaG)q?i(K*d$EBt<_Vo3f5CG~~ z@#=KrcJ<8%_CQ173;?DTV1gmf#_24ma0dRP5`$Vk@cS^pqz8+e|NBTgM=U8Oi`SVV zmmaaGz;cvCB@+LvH2i&1OfD-_MNaFE?^8t3LuhQp`YtO~l&w%P)4hlooiQu*eJxk# z*F1p$MoISSVDG{Umm+&!=2!`Vty_+dCfRP#B)DuxI;Fr@R8}!!RjI5Ik%DKsL*Rb6 zuH~iFo1M(ljtmdS0WBW$3#?i8+s9Cy=09+9)H#@iVY{P=&f1FihU;enEU$gd!j5hJTin^s$Kh;-lD&Y|fOnJKa?Ml9t zRO@5Q>C5bwr++@rFjecmLwS+>5859$>2P{hi>IHPFjwY_Zv=zO&2Fn2j11SU1Po&R zOPj!StF^U3iholx<1}CPm%Co)t##+CrOguoHv;va7ynlDTynkl*Jheb5AoW2(5B{^ z!-Kf<@=ss)wHuzMn}%OlmB>}NsU`$3K!-j5uI*i3KA*WD9%BZ{8BOC11T2{~zzYde zx>p{Z!wHd9650iEU(3cc6Od>N=05w%OVj;^aaW&&-{%h(E0seEpGm!^joB8lIl1)( zF<+Rvs_IH+2F*#h^y1ySL^QXj=VTw>Wg8!yvOHzj`^#V0j0&li&OB~qblh0KEgnHg zV0h}~f)|g$E0JJleA9(YRl{DK0rZ?(px07+dEmc~55b61hehLx!shEMn}!vZofq#{ z{UjxkF*lPlzUqs!|;uKZPfwy&1#x~fuReJ z?d;Ydx1N?@G^ut6cnnH>L}?v)czApP4I+EoFp^y@^^Y5D2A%F;+cX+hy_1u3{qms` zr39ID_Wb$hyEX1w<`*Qq#af4OX}g>u0iJxZvOU)^ zgj-4^;!lwjw^%Yip%?rqUrLqB{kzvg_7e5|kMms!5~<8eb+3DSBtcVpUlw-{q%v&T z5K5^b%nu;f6`#tSMmd1%w-Up;cSqgjU^#K%b-VW7Bn+R+@9>QXAsvQ~iHV-|A8s!ITN6k z(i`ZaqB+2QfAjVhBbsFEmlW=YH!bgb9`8!&B`^F1@V}pk|9h2!A-}xkOj7!rweq9& zlB(rJHYx+qFS%u{K<&-;`LuJ~97XF(p*`~%I+|{1gqt=YZE0)_c$}$GQ>+9%-rc&@ zoZUheQz!EI*`I%kvT`J;EYOLh1Gov{l%0w^TP4rg9#nJ;<>f=c=5k#`MC4K6kq7XM z-vU3z(Xi5APwv{F_XdJnv)ejF#jktDQvtCkd|jWtK4o<6{Ar#2SaFu~hhMTidw!EI zC{vOXPu{(>Tl1*rm`b`P)lJ|2n?mMfs~W(OM3S!mM;P?C>ad6YdAHeYghQ7y=3#i! zBz`-eF4)XI{bnS_1!dH}{ZOTI``p3VIcO6UJ-xMWTXaR;&62|vY%05x{4_A2cB!FO z_xG22?j6_GM#{Xg*@VYhD=YVwLvaOcQj(RE>eae0PON7vQPJEV-@Sims53TzIUz*_ z#iou^Z_&RLBR?c*g;ey2#MC`XQn_L+YBuq3P6!U44HGYOcC<_L1tybYFohcE>cLJR zI72r%=_SX@^-%ivC0c>uxn9a30KaPd1*Qu|x;bD6AM(jK7q3@w$B%z^C=p)~-#Lq4 z+TV}WMZ zd2U`)L;8S37!bTqMhOHs=@w%l)00U^_s4Y9axD2_kMB7j;&)pk6BPH-R=P1*RUS(n z2Nx3+b)KB0<>KQxe*St@5lC)l9vTg!%AMvjAF!Zpo$U?d{lzQ16DrK@)M$qq*4ORt zKpcbJD^N}O@JHfFcWBS~A%UaZz@gp z#JO|9&;5I?-D*PmHwGmZQ{gI+nKQqCA31t8Xk}a!yTcp3wkh#cSNA*6bLC!1JGFhN zl`g|0rT#EiF_x)xZLiV;Wz@7;*56-xX__iB6TpbS-3p#GM3tw46>2kn5{PP@4qBHK zdp>~i&%G-)kR_D@%Frr=#@ISs^KojLpic+c!wead@}otczZ#deO}s7kN^6!o(L7K0 zOZ3QQf@~*bjDgYd)mI}aNd@@+PhI9#!=k%j(bHK;n&=jl)Ye{UeO4sCc~+QbpGhk( zLjrfUw?D!$(rl-;oG>mbFlM<=mCtP)^f-)lM3fzTo07G#n5+0uCokW8A3Zaf?4>v- zq=AUj4BMIj6A=9(jyF_Xt$3f(TVc*}Dl12ZGQ{|qObiY4(6EFey&5Bm2>b9x?=|tD zqiTQ$*gcqXPWW(@bu@6?EVwB{#6h2vQDps**Jhjt86N|$e*phKJ+2_tsr4~k$ELis z;!MViR#B6IrUBdXS6*5*Z-v%aE;CaNUszq<6hU;L(Fl({Fo^fu*5Cj52rPv0-@onK z+?by>`F+*b(>vrye(3MC^~D0GfYt2Tq?ldMR)x-s$1J=)IcaBkb2V`9a3oKiW}%B< z@T=@Am+NT3m;XMlv>J`>dlE&qyTz3Y`R4s5iOj2Ty?e1bM3W#S6>o0+^ESR{*p9>sl}Idli1kl zH{%XDe99N7AeLhiE6K@B zM%OPl7%&i@Vij|Spw(NF)@(6n<1#3}Kh9rgV8HbrDem+>squ}MT``OfxlfOi`HX7? zNdB~ae``V;rp3%g1;k+Y_{hy65a?0+FUn2n?av8R-m`&X8y4+PzX*4wo{IYrkHt;E z%u^+B`UyxeViP1B*`POB0%%_bExhA#lxd_riMjBnjg(1gxnR`HAkH8Y5B}=(oA_{J z$)tA=aFMsTIcv9nJCr9y@;6T~vH$PV%>s|7X77;PUtzkz(lJwdgH*@?H!sev4WNx_ zHn-}R*Nd-+z8+&BBdIy*;HvXu_Du#SX$ zjju24{FGV>n(( z4}xeWCjq#(s>VLc|XwDQ5|g}RrE zhtxW8pY7VPahr1ivQvss%5OR-(hts|H`(VYFY0}+E@?aoaePtX;KQ#C)>2AJiemJ@ zfcw1o*)ak-SNY(l^tBX3M-ipv%Jf|z35X_<1@i+>vH$yy6un^f5P^u2Nysu~%FBoLY8{)Vib69Q}SkbIcq=MEyEh?*HMBj5;>+F)|_V>BwTh!XTalPqS-%Tv6c zWm=3-w9R+$^Db|pmwk_T($>@U_N`6jrOV&2l_{r#E=~LT^@QaN6Vuz$^O;icEDd1r z_fIrL$}MJ6kBM9%Wz_S+2zGx4-Q}2>isaGMnXV6BQG%$U5j2{6{O80W$H5seq{i0x zV1|agzknxoDixbU1`7Mu^n-yWhz~KDe_dl$p>D)mdfKDTAbK(%Beh9JLqj9V=KSAc z3iA8}Mh*6~o^oH@ocx-iciAqEyk;h-K|KtwGj8IYMe<@mU7eY9rR&cNC8yEzT2w^C< zqw!b!edCwq)!<7OU3>BMF{kt{Mi(N_gILtkrdgl-9!eD_wA=O3@aM=#<(+S=?R&|j zc2bwYd*5vb7J>hIFNYXx_w|N{PmSEm@Q_Bt>#;g=k{7cfkv{>l@QWW`$-Qk3Au%LMrpHZbx=lhw9(9gJ3j2Q``u`PbEV zM#3JXL`Szo=@y;y_4NOBz2tdSoUvOLBSuS0n{8b4!ov9Z8-@Sgg1z7e3Q<}l;V(($ zSvPw<#7XWz9$QaSaAU3#ALMtQM0 z5@^Xk{>JotEv*p-fq-B>JVPeXkK}QGMn*PU&KN*FIqUBT!NMr@uCS7M{ymHtd*d-UA8c}y|Mxw;t$27C zNjwLol@MBS&gGG}b6)JSK^->EYvcxs*>iW#B6t%^K zHII$7a$YX(^!gWb?T7KId=+Q^_a|*;)5SDN?rUoam?0!H-EYnC*Rvqy$t>=Xqyw2G zcncVHuGA*=Cp+yC2n&y^2mNtXS4?IaKEWFFxM{*ySDuH4$^$sN(jzdWs$nRjsqdFL zofW3pw=iWSCuf#=*SAPd#qSGNCrx5eO3cm9YN6_j!en6I{>zLjA@lR|qt86+3YmD( zADyw;NMs|vr1fuwlD3h<-|R}fpA-kiw4wGcI7cJhbbl?Lo=OL{(vJN}4OFupoeTWI zdtWb#VFjpFvUQeS1gmr?{EVp}-LCgiS0{SvCP@UGr^VmiWKC=8xPu2#is=F9!d-lM zi&1iqF?vC{!}uk5DE6q1cp7orK>y<`{^rA+XtjO8@)8rCrKKfp;Na{3{`OYjxIbJb zg!0EgTef3u)Gk#7^;PCTVLTi>p=gfv9xz%y zph(j*TnqpzOchLBC#KHXVn(Vc(hz-Fp<20mLeh`p#>;czyY7rng#W!ruBkkfTqC4!$&%_CMy+XMXOecUu>8adV$7KQ z%Mh?v;g`G~Q{cOx=%nO&M*d_p*QcH_-$2brx`WZcIN>I!)uX zJ~L)DinR}ewS}pAvEEcQd<08^1;`$wL_(*1)=FmkrMD{K9e?Yv^Q_`x4T2y;)(=Vw zRGu^JG-1g{K?@0d=0wF(jq(M?jS^qMb<-><5t(y8&U^dzt@Gn>I+_Rs_2A$)I4+V& zQuw8#Bj^A9v7umSp$7T+Ae0qIKr;UH#}*vCV2}#~lP{e9`x_7Lb*gUeFZ5c?F9OCWNtU-rafv*Q+>e^gP zH2+Pb6iPzed|eS@LqF&t#O+u_DZg|$?{cx>5>P=bMmVxn?0@E=aGfaNs8=d6Bk&t8 z;1yZsMEu-}yOh?#V&pA}7eDL4()h36)Le&{#h&ATc?Zyj5kjtKO@~rr!cwi=T_RM- zp>L9-bz*lRKLNPp#Flvx>+4+iK0RPDzK_r(6B)Q{pZi)Wo>s+ux1g;JUaU9y_qA!( zEJ7kpz;;R!TQi0J2ldj?B}j{qjz5hzyQe{G4n!ma@0XOeVVump+vDhRm& zE5D6hC_wqxoLS|OjL^{Jq}38xgEtuRutZ$CEyb(6_fry^Y85~C?;09{HArNT<<@pe z2csFu#LEd&DU^?|clj;gy&x&Ui){`@*Ae)10C!m1D0DwgJR&BRExHyxpOmbvdFK%N(zkEqI(2kmy&=Ie>d_A`I%a_*tp%qr~s>)L@{FMjA zmX$PWsX|Kue{UinGmE_mO^U7LTTPx7h|pi`WONjSf+P^|gD*0qaj107XxaT?74Un> z4bMbG;M%;MpL(oc^`AX{sykS-c0(WvJd#Fc+kdAL0~CBT5Q(iVU{#tJj0n>3kRYLA zamqIR-%9Vg<j>%Bo0aS*z3SL*E7K4xXHdT#{+2M-z%c+Epb zQGK%S9jBpD?mm0K57Hd(xP|wX{r0|LN#C&pq!u5pG${|T0NU}CPDtP&8JfOna8!;U z6QNRb9(SiKetH~`?fhp>M^|??<=L}0Em^Q~!u6Wy+vQs7u3u@goo~SV5@43Deftxxve?Xj z1J93S^Dzz7veh!1DzB8ZPr#|l8@VNp6HZ;s2Z|EW32)#h6{ANY?N|suKWe%e2M_+ z?lL4t@iljF*?%5hQYMBEE?qy8f-nnPBO;A91Kk0EhXCRvyI!^xv8vwhJ35u-jX)@K zmaWn^!Ku;%|05>Fp8ue%!UGsWO`kxns!K>n@CrN99qog%*a9SzaJveWlS&oY>Ng+< z?XM)Okl~{;$IB{O`@WQ8iQAOvio zt}kQaB#1%f6}^MWAO+XNrLVXI9|P;2r?-H~gI2x#?ex1#VNGyz0secZXyCU#A0AHr zzaJC{W<%@{cYUx6^mf#!?-p~t{>Vv>`|I0|QhqH)00prdHO4S0)`|-Z$;fVQuGvgb z6K_q*$6vp0`1^md`g`?%PmH|;S?H=Qd$eFnuiWB+no6VeV!wpw?#u>c+rd|5wvUYxmf>l>k72`#~X)9q610#c+CvudQ+NT^t3kf5mZe!QlKQAvY ze-{xRKFnhF?`hfCTIV&d#C`uha!IBKb?J-r+T67ulGwYDWqipo^$X`>xjy!-xw)!} zo#OPv?oR67mv?eBu1tzE+)|S98kpmD)^i>Hx&>O_*MHxFyu`sj@$s9UY-MnqO;LM; zOnF=yB^OOyS!MBh0bby2!J&3ir5thN{UdNW!}|IzZiN1fK|n{}ohPtC6SeNlpPrt& z&|kl>NI0RLqlgx^K8*0l#GQA4VCu}jiJ`(s8xgD>S}5s_-_>iTnscs&a10ezIAWWg~7D*J0x#_ zHmK{p^^Pq>*mM0!FAKg~_`Y`oC=bXXG31tEh*S24POHZ%32q~^q_n#>5S^z_?T6wX z0;1P-2rcxJp|w)AYVnEw93C7a!JdgnsH@!(cvi`s4$!VNhkqZJkP_$_(G`uZdp|Wb zM*~X^SV?Ybui1FyeBi5RXtx~#=8{{4b{mOE{;!o8`wX4j#ccf{mPTKXtCRv$?<25U4N`V zzkst=V}cZt#TBsKsXyuEN{xB(;sx8-&cD!c(Db*JsTYk(x%Qf!@m_-rGe7d1RX)+prBPQ|_zWtS7pcJ@@ZHDl;CaUEtEu3|?Q%Nq^fgNBfNfNGEX)gc17zr4Uea+3i z-Pb=gy|J>bvB(b-6=!By@BVzR5Q%id&ovDZ=R(rAG1-f>C{&>VFHCoD{+Dicf89p) zL>sW~aemtI_jdU^0cS&U$bVm0M08R<;*uV263RXiQZ>)^x39(0#AzKormJQb$n+r{XMKa5j_y|vQn z{#_Ce(UA;joMP0_a#k~eX7xY;pY;0*M54nLk+pp2&b!YFQa)`8?UsD;gz@Z`ZfEgNLQp3;`?KCL3ubbk)G#&Hh4H1uk zz}BfmRBjO#ou6)t;EuBDQRfT~p=1~q3>?fFy#rX@fJvL}S$iVpt0?$AN$%e0kQ-d3 zYmRNF8SHi$nnm$PSs7;O_on-MazfIiy}nMdqQt7Is)Sv4tZOXSeqX5N4{3XEr6CVq zJZ7=YV|#nE_NZ~UU%H^IOvh4x?(fO0>pugR4E&v48oE9l#>*i>h34|QEZwd+Waj*2 ztLEh0(XjWDa~-jO?1f)=Y7rqdLUpj6=Z{1bIO{)*^2f{hc%+xd_3uk|aP(egV(4R2 z23?m>xw4F@KSh+10#jEaUUBRVmUu^5W~=0VG7b{-Y`X*;T!%T4;>x-Uo+|OLrP3(9 z!#XIGm6h$LfVb&?o{N7&lr8IzgUXHvq|C!P$N?!Vndz2&4=7VkbKek=CJ-ukcvMYs zpz`m1mY0XjoW6Yif1BW-mW z8rzT@z3Ct4HOx;cWJ{39EFjf#THvn#&sQ7G^JkMw9h#_?`!*tLP7}Z}`v6a<`M% z>o$3&Cmb5~RpZX5OlfsNsSIB>y{^XCCiy0?E4ZX#t2yOs)4?;}`W377uvT!b-rTJ? z@sr1l9Wn?Bc=X$Q`8>1DZzt_yX2ue*rBI4Xmjlu(fx`OabO`nNSl=Rjh8zwShoFp{ zKTL$&Z%F)C>DZcgZ|rI!;gu;Odgzr`hBL;H;k@>~eyw-_ZUak>Ky5?-(?-Y!3&387 zR%Vj9;%?k9!hjRV1Y;_4@8g@k4B+aWduWdSC@)vSJjx#>l#nuhG8q5hQZDK@Z=g{Y z9lp*1i7_QD6|BWH1kESokMM}Zpye!gDP}~$gr$yQr?<9_sX%KX*wV*XflW^f^bd~2 zV@?;D`bgwtV@L=EDs{~sy|}VXt@x^+0+HHq2?FXLr{V7PZNFY;oazxuykIq9UFlJc z5*v+cs#XNtImzn$RyjwuXTiU>nbagN|90F9EvBv?=sSs1Y823E;vi7_p#5smw?K%Q z>tbXykD5Qs*BzLka$0k`JW`mO{qQ<4BM5nf^U1sz7{yNsU^}e)>DR@7-9n$?ia$p${FANa#=~`i->(#MN zq)eVqR)nJEkTrJw@*qeo{w-jS>C~AUK%Hh$l$iCs^GGM%Kw8`InKo3fG zp`J9oG?aQ^Ks+LGV)5$}k1Kut(3`M_PMEUHRZlj9k>N4PvB&9-_ypAOfQX0hE@P(a z>09It_w4-KGB~PrHDqZ?@d?5EBPMVNU%@`lxcInJdXWoL72#X8q}d7G2i4YD6l(4- z;c&R`0aJDCu35kzA@GY^y1~S!?px-p=JWHW-Ve`0zG7H8FB#5!*!|HvaOn#O+|~Vk z>jq9vC6JkDF~`F|04SDweZ@}5hI5OsE+GQmtbdjF%=vc=y?-Y?5Km{V%{xmzT$=60J$9dh>1TL);Q5i}tFPZBYbL6FIkQ~iu3wKV0V5;*;u@P$Tl&kO zFl-t1lEng`7*p@Drd}+iT8|mhLm)DbZRdiBw4QzQJ2HBr-OGPQ`SRtM&B6%15a#af-qqby2 z4OSZYH)@Iyu5~vDd5SOfBa`KQXQz|^rP*2Zo8c=Q0rFKyw zbVZL#wb1sX4Pyqi;I{3qol*rJDg`X9dXx@I`yUSQU@Ke_#-U8o7g zm#h?MsipObmho@iyPqAkds%EK4dc6gZ;Wt(d$XHvQnAG0S6|7-=l0+z zHbEq&!qk~pz?UDg*z$+Bu&e-g`<0rCQXWI2pF_Us)*Ar+)@Wf> zh#9m?my|g;C#I2vn3`gK;)JcB5bD>xHVc4(*k;d7(7uSi zt>cVW^YEzj>k2c3rQ+6`LJRmw2IGLG*uh$93kFz0-yO5J$0JWD7WaX5v|r!IWA&%5 zf0Woh$Ef8#x7}Q2@b&2L1ch73@iBIZ&~NdanZHK!zl0k%w1{>EV8P0qcd|Bz1zvcD za@qjuRK~4mcb-9!7gNK-d$7&ssk&c{`tjbS6PsoA_2Yd>LL704@q7lc-x}^d3D{}m z_^8U+7+%?hoCXFxPQ?IR24jbBO>4L_#&&!X-A~cbR)3yVH^pqweNg!Uwm$);jf;s} zHMr>Y$|6XNkc~}?o9&D{R*BI}`#axIx zs0%HD$r7NFUYQ3?OXpC&HWgau5t!9C*Gx_*bzf!q*zM~JZ4s={&L0v&guGr-0VKS; zz%6SIbdpqz5)*dot&D(a;#x8q0lb~QPyiiJgxdWaX=52FTS{ap#nEnt_W4Fp=@QZv zR|HL;bYDBlPxE};^@gHT*6SALKW|m>P^#DJN}r z*W9Gp@hKKkGjJEx6~(+p&-d(FG>^cA9nF!waPoS;>Je7YuqR~aCUR7~x^hwyunWY2 zw*1ZY=B_qW*V&(Ljsc=C0M0!bk<9@df&p;s z9~eYe?;7v!CO+6N2NJ*UXx;C!!aARPH$Yy7UDDw*_(B4^pvby~l;YwmzYkEl@p3Rs ziw4Xnc|a$UyA{+A*f#wkYM4ZhWZJF`-@U>xgtDO`Syb{CHxJJd7Bp8Ry^{Z`NOpdG znkc_4^aD=%kb0@l92z+f_FqD}htt-%GIz>*{lJ9Z^ndWMEWxEGc@eHl6y35>0F4y|`Jq(DhJsokrtwt0{5?k>c-Z86b!V6&+^uPsu@ z_JTp<=dZarn@bMDwGE5BsBa~0sbuWIfERaDL|}&dS+yU!TwFR(57eJT_+Y$#U_UPjF(>Y>v$4r{ zd6a8}?QfAylZA2i_xA3x2Tw2N4?X5LtyuE6e&|%kdOxEEkId(J6%-fm0rE#g?~fl} z-*itEI(L$V8mGtp?8qQ{=3`1S?(MO#BMa|)JK`s>WUyI!S<<#&#(8&J&a?I!-p z7gd*>6o@rsH69;3G{DAy#J?!4?(*i~z>~1-ta)kq`5`C7=Kv64Flu|eB3LggCl{}F z8&!a&7??+=o=jY)prG;yXkEP~>@@JNfcJUTHwp%(O6)tdLD>yH+4LhZ+ZciuFJCzG zD_<~wn>>I=g8Lx=2~6D3eq`F{wRCK_c`u1Z1WUMXb~zaqXZ(FT3%+@o^d6d_s}DR{ zRzFOHCb+XD<}K+_l1oYbZ}jDN{qFKe8q6OW0JORxrBeBISLIjHz88l({Y0PX6 z@W)0of(bjTnB$XDN3=CQcxjX-Otnk$^YNcc(w%t-r$yDR@wvq%=dy)Al+`uA;5;^O zcmE7XZfEOu@)1#Ts|C_YV82hnY5#lGo&Xsy%&%Ao+sT&BduWA`AZx~fr392!;DbNvc$_Jh|=uWH~eZC?5uQnpS zssIvFiw1I_Kvl8fwS0GpCEW}?Fx3l$gOx=mp)5Y!n|58Q5&bf|fq38&_=@-A8`axWU2Mn5#0`+SKcpzxy#T1a@XdEa8GG>oA!Hj zG}t^;&3ki0Ra+Oh?nq=uBXCu$=k_6CFMe>X$!+^T(w09!rg+i0)oi9;w~M8D>9a#lW!bj)!sKYfH#%+S`_BFs9u z9xTPVVl!^zwkBvjJ%{5Ft*D8IW&~^?1HQ(NCm%_AI#Z+?Gdp2UPw75!m3DQ6O}rf| z(9I4J9#q%iTP8*;Ny}b3oW%E}L8~eKCtb+~LYNwZb;u2i`Z$f4(!0DCYge zp3?Ae=lJ+D9zH% zOVFmC)f-zSFsG;^@oQZX3F6`{wb`SQBw@#qhnFDkJWj(_S=!8uq2fIuyQn{f-Y!Scf`$lz4cz_*h*Qb(`F+lGD_fWaH|Y zF-bRA-Qs0dL0wrP0k@Ssy55O#%zA1XAlG>R1H4Rw>M2@FIy)mTXSEsdj^yt9qb+*K z?>RfZWJsha@&`+rAk4Bpdo-@XNA-lHc11ab@UkCOyEL(so|`q7mnIT^j{<-~YQ8Jl zOZo}CcBs@C5Kr;kYLI3+0S=CRurN!l7~CYzhnZ?cdq?kTm+X0 zg?}?q0kUP=`}5J-l~gykuhFH&P!KgRK|t&Vqf6xZH0Y}?x(!oti|_RG_2+^1n&n4t zZy=ki%w76b*d;cfQK5myS-?%6LIC2M7k;>2F%Xp?V4lv;^RM~HAM)ojIS`k2kYm01 zBA5HKHp~O;)ba~x7?=klZ7{=x%j>MqdG8eZi2>in;n7k3O>`+3%t)1bP0fmzj32H2 zFl#Ei2>`1LjoMQ;`HRfZxnCbY@?09u(<}Vh=_%fZx`!NK5wmFRR!O{fd6_l){w0M0 zK0fAKN$R4(aZGhU$kop%3-AZDxK@p@BX9MO}wdJNP`>uYc zJoMw8B_qOsQ0f4$ZWwOQA%g%9%G7rsQ^Z@oz}_`5SX|N> z(4g6sy!-jH5$KEH@;q~B%lH!(%D9QZG#w*7H@=_-$Ki5ZsK&rmH%eC~CBtdkC9KTu z(Du0~`K$-6w?OBqu9=F88wXrX`Bxpi)LvP;#MXTDluB5zwzX&V4tsDg@*&Is@&)2* z0@<4N6llcuAO?U`Z3yaWKuA>ia$!Pt+X590`1nX5AvCV90^K-Pv(#T16Il zPEf+|6^gBVHDBws8|NI}7Xp9-$fO?1cf)4od**>@>9wAor@?-KR1=}CbAf&BIqFE?; zx{g#*wjY|_A`n3$>(KJpH+QgS&r6TTJUdky&N6%cKF?_~2)Ikp zS6B9~Abk!!<9}xOD&2-cN95VVwO-%{eaBm3(Y}VxeJ}BIbMLJSO9$aw4PW;eMNgvK zc`dRgziOt9DhrDkT)(7X;B}K+`oazy=s6C(ma%I#VKl1+d8_(4**L5q-N2k{3D`S5eVwWZ39f34M(%T(urJR@Mq21CjHr`GzuEM@Br)T@%h%++w>)S^5aR%; zbJ(18Hr$=S6E<#=eCh1$?21{ffd0y5)~nvHGypZxq@UKoZgKv!bIio;!knB1hn#u^ z!(AXAlFxoW0M?(1v@lKmH?5z)rzCx7YAm+gMfEX@cX`+^BRP$O(B>dAngh%7lRBUZ ztn{T=KE76$CIg1HZVi8e%jf2N5zDK9m_n!fIL|abS*aP$ktil67tuUDR6dHwvKs&r zt=RL_E=>h9{R7oylM>~H2@$Fxit2R&Qq*US%=sTs`PjVY6+@-J?2(4>%W?8OR}+|z zS!oO{p&3r+0{wMpU~i76lC516l=n*erui7W*AH}cX=^w+4Vt+NKf*CeMsK1r4Zx!GYGw-t63DY>w2`Rw3$J*Meqg z+sF)MQN9CpFR_jzuC}{-j)wPzJ9+E&gYsOChYxa*_3sh3yo1_|jEpriBDS1=2W13* zae%4U^O2Ss_A9kdtRJLmUGcC?=~{oL-qm^-uc;XUAfMWnN|sgSf~63~*Tq(s7yyZ=ey7NPh15#MNNzTz4B!Czvv9c}JtcfT$*XB!2dKdeGGz8EhaOG^OgQ<%8DT>agm%uz^E59^CVmmUpkun{ zxIHP0>&zRaU#*V~_wN&EJ&vVQwobyy5wO+Z4)_Inmc>a08`QP)nO{rMt8NIs^1K~f zg!I9xzQGYx5=jh7CbG`mbS1O2BYslnWRiKjdz_coGIc&X_;;YCUHJdHz?W=p`n+kI zYG6SIG0mb#BA<)1GyX<^7V`;BG`n^Fuc4Lu&0vdr4Xf*njzAUq`hq->H4JH$_0G-r!4i2+dC#`~Ws%33tU!P0TafntM=J`LnHXgK%FD|W zS5s4OSr2kiCwu(%NSzlbN7@f8MJJx2cz*sIo-~QtsF(lyP75xI29L|Xlv>5VP+A0ZhU zas0u8MEz$D0lb|l$t{3Z+`Mc+^>{|-xg1j~gqi~KDmX+?;Y~Ydlu8*+Wq~}`b8iPq zn+lf;hX%VJPehiKTugXN`JUdgV>$$s5--tUM#NE3oz3O6s0In_lexpi%q|w+)pt}f zG3!kH_}WfGpso(%yVZ2K>D6+@y@UdlewwcH%%qXLwcBxM!IYU#4+u>^uxX0@CB;a;_9sqT|n9q9~c2VHK8u+IIZy*rPPj*H@B87K7oG8|s6< z==)S-aYHjbH#)@2kAOvjYVZ?zJOUnMYWQh+X5CRKFshw=vGOZd2(NI;di3c|ov}-k zXezkEq0BKEEpNlL9(DaWr{ZIB>PyP%{&Q)T<6(|V znyAA>RTfay2Fx{Gj7mhkM~q2^qRxqT_@%w1#hg>0UZ6y_rd+XUr|-4`15Zfh>Sse3 z5Az&Lhv6!b3~>kN*bd3f$bS?9YE{#I#5nH8=ji=HKZB6LG@UI#VcpR9)8dC#W~IfPkm8F^26Dg?{EeaWcd_?GexbhKOmdE4!LmEkvjYN%P#$h`p}x{c;Im7zGd4b@xb%b zn-DcMwexU;Kz8Uhh6oB@OGzmmZ+_iXZ7lpf39!(90@r;d!1!9R$<3|Ft#0@`u3JwxB;4h5J+VfFhZ^ZI<6r&ZGN58jE(G55H^|-jdw>*RAmzw6b#X*a&L#9 zZi;wm0=Rc$AVEEVH}Lw{z3Bk{TLJq}LS0Qg^csW|-%S4M=Vt=B^+2m2r`Nr)IN`WZ~;t5kj` zj|>mLI(Yc!p1)6Ujx@mtmww@lL7Eutg$#IqYLpxIgopEe6dvrgOB()6pN{W!E5y; zA1Dap(xz^oDB|nVygd)$Qqszxfr<`Q`#VwzkKn>84{GZb8ZanS-pyu0Pd((OWDllp zMZF7qVS<=~yn$2h55P#+_`T&j)B464jL?J~I}jX(9wyX-Eb9S^ zNM!%X+b=$=#2Xv%@o1Tk=_XC%TGT@}U|2vNgm)+1JKkSoWfAf_j6goSraU+c{Wt+2 z;b}M$!HO-*$a#qhtxrdl5EF5~v9kFPR*&KF zX;j9A3M5I;@@*inS8(r=Kj?3-_$$k+s0HTd0^Ga|H;Hx?$-L1HE{V?fVI6g`vD9w` zcj;eod)8;l{)H_rj>)S@hS%b1yV}J>f6`5J~98y_HcxCxH5Hx!RE%~ znf%?$fk5+mqM1)&;9~sa;+EVM+pk2x0HVbFV)7esc4%7le~TbbMktX9`S(-apE_?t z&NXq-cxxpOcp$;5W)I#rHTT!BIBYf|3}Fxt6D>e)7H;Kp5r_n{>FlHY>V?h4{kmfa zltQQimw@Z|#7`az!UhRi2Ru}I@C5)1wcw9GBB5kF2uwb5ke!kY7Jg!Q+qlG^QzXz~ ztjrR)xpmOoy}|b^F7=7wB64E<#e;$Tg=>&A_o;gM(pyrMZ6Z{9BVX^UP0!*4*b!dN zs2O&pgnOp-m1lU7IhYVu-+<*!VC^+EBYiFYd#kxNK9P78Oek;Bq$MD-{(~wa89Z@v z^XOLYSlm0+g|NKp>i28EOE1oj0hl^JHXoIuq(DT|RHVQX<|h;m-Ac7GyiK#B32+FG zBv@Qme|&g1E`*BtnV|dOc47sAzQfCxCV(c1ZTXTG4a!(7|2X6o9&#K8ZRD;FGc@_f z>Xg?f{p-~1;ZREGz$du8v*U}`HzCd~SQfLPUy9%oPVsO5Kinz)ERy~xW5Kzg)o^4c z4URmXBnV{^ULCWFdNd8#55Dz3rq)TfYVafpFqu{Y&}rad7ppb}@}d<*BTvCgfo>i@z|UKf(Q2kA}x;sU5-=92Db3`t2QIv;U1> zH5)(YncjA@hP}|@4K_Bas%jz!^Uc4+xpkWf&r%+Nj4$jmaUehrQMo0$xM6&;!U7kJ zv`3}!e0&o(OpT0;jsdOVtWyrva5HKjEaC>Fp805Vy{vZ?zSOwo^Gu{GGyVXxzJ{xaOS7c4($pXJC>O6*H1mo(sv)q3g&ivKr82Xs>NaD2 z2O5MZWh(y+adtZHq;*rL@gi1T=($C^L}*c44w zA&;eGC2{&IPK>$2UyCQxxp#kg5I6wKe@|^~r|*X3MZbOL{9X+DoQ6i1SR~_KD=w6Z zS!ySVhC?pEVXA%n%)K4T1_VhzZ#;t!jz?b45c!CaZt z&7iu%FS=tA0NcOWMAT!|*_id^fw@zW5f6j`Fl_>X^{CvouKV-@Ql?koyXQIU&aw@B zwPC8NuvmR)v5SlO{g=bs&JGTrfJvHZgQwFbdsMz=%6BlW*!!L0;_Tyac8mpW_r*a$ zc676}8fUtP!W+W&ER8NVa(0BR7YZF3`fc}`H(M4_Bis(`dy;dXY4xDud$RpJpLC$* zHLWPPpQQXKNM%96GKA2~Vr15`YYmuJFIGDMs=vW|AHkQgrus{bI5y}DvLcgfQ;eN| z-gc#=m=E}yL-vId~d%NZWF6tKU98`TqU8`+-#1hO7ezUKCDh zO<%u;pyBBMMScFvlc^&P;g;!EML3|uIUvM3-e>CUrbIDuW4?%2;eHZ=UoGs^0_eFo zRIV~Ekx(-1g#rs!7iT9YLr4-3ZiRcveJlh;$Y!SZt{5ew%pno0GF`c{s%(>_e)ly6 zriAA#gBZ+tD_}UAme06G5nkZ+ z)j;v4r1kR8Bf!AmabI=#J10K~%8ruy(FAt(2>Tsw|G8vB+x6!_mW?<_3M;0VQ~oV? zv1@H&0(+HN&QB~zpLrKB-KorKP984#ScFh>N)smyptyLpDGJP_pO2O4M>7g3HlMy1 zXvDQ#p?B_kk7CwLtf{KP)2sSYXtLbZE?Qcp)`#$cZdv|-VBVL&8{tQ6qi z9Dgb}H$ClfH;o=;atj&Qtfv9BfzAlW^;+IrZKs;Ldyc>e#}p7M==$Y1D%4@X1kmh?>Sz|Lnj33DW#ikZ~ z9WV2Sqk$1+8{jcH@*z;p@c)mck_2LMa2Z${-4@87g^qw37P6E1a$Y*@de?mNy0jmI)EoNJd%N zj7+FKjABZqi1#cNZdhwlrD{2{4OQp-Y7l$C9Zs7TZXtY8($!a?{{`o%HX$ow!>8-e$7=Xt-HH59;{2{Db0xJ{b zelYLRAn(gz?E@)Be$=@04#92EvYr$O({-n#p%~Yc*sx=S0Gt9%`*55CPi3l34h(0p9V8HYmgTIB~173mM)> zY+e0w()fX-uPF@#z$o)ZrDkUVKye+&DrM1Hcm=BQ`+G3$(9|rN2prz~*z4>NX}2HC1A}FCvwae!%47wYGlj z)glnW6%mc0)(LG21UVu@YtlXg5%4Gg_C8?+5}g(QYmib07jqH6?U~$H{N{>?w+JQM zXLl(eu!St{Iqm(L-qQ5Occfg2a(_C)r3qM`=(iV@ zpNU1TtoS;Dm8yFY=BLlsKo-H^-I9EP;PpZXk4QJPwJCzM-U-CBXMp}*tVBxTLkAm0 zzQoeF_lZ%4(w?c~opS({3h@j#D|!4&UEneV_;k4fTYeX@uf{C_J%dD~a%wOxWenBU z@D=^)lkB+v!nmJ9#}!dEMevJj2J{3snY`s?PISUH+IR|id0yp2;z2#un}&s_$D_OF z+nWucA{A87g(j~!HHsiO$Gg?{v~8nIyOs|w?erME^7r3XAW((IzOtXW7@hpNO=n^3 zQ5tFXdFp_PC}@*-drG^%K|9SDBm;VL@E<)RlSxhmz%RyDKfC4`bvw9*Qhp9mdhcm@ z4$+CN)1FpfHNYeMdiPdlPe#qRe#-%~Uc~CO@F2bX{%PUo!qi@k`IcMWEjkEW(fjus zV@_i5p75KU*o+GS!gDF+uDW?^-}j7G8s>T+_bHOq0#_1w_^eS+l8{msH9vjq7LU#) zh$|B=F^C7P7_QP&$65-Uu5QhI^8d1NPssOwY{w#0=;<$D$rqvk{6VeP7{WpYvomvZ z@8DdUHcna?XB;Z13U32v6crfoV^@7Hr>ZOkab#N>rEZJh8LMkf3vCi&YLEDC&`05%Xu;oiFot6Iv$U@_=HE~t|WyGM`JTmL#j zq#_(|XI)%IUJL>+k;VV9vLiYCkVS6g^Ah-#1OR23tGD;jb%r(~py#MDqUo2UK;7nx zj4Y|?PwV~kwXwV=J;DIYwg6G@Vlwm_m_dpIGqtHre`Qejz^~Bre*oefe(1<_csBuh zAM=+7bo_L?7-*!?;c!T+ceY9f5TBkWBnkZ1XhoG|uYMvBX{bO%G^*(MC%^(G$Hwb( zh9ZA@t_u0ye-g#dbCdwVg+Jc*Bco>@g%Z^UPXCM9 z6p@kiud)2Mvuwf|Wuh9rK9-PdSjn>pRZy|3GIeFO)(pX%7ISkid@x5?*7Pg!=sP3d z*Ka6=TM>~rIkS?T%BAIa#S`rJ%%6G!+ zPmk+1R0Ie31z7174~+>QhN6DEyhSaWXD~Wa$Ihg}qi&HmY9TMqMl zn#l)(Q8)aqDJlT}IU1UO4DI#dGqJjrN077&Ox}U-n@OXh?A{+Nh9jk6ylgwRa)XlE z_r*(70F}$sW5n=eNNU<-Q9AzG^%A2I{V)KSwEX28ojA10U{g!;KlQZ0QOd4N98vQz zYgVIO0hLZe^XLm_%dLyt%*;%F#xYL=M47a6v4U+A!$zUP>vjt9THqUQ?-jU}7Xf;r zDNu{Hp>1qFwVp;$x9tS0j?ne>xma>dRBTfHw*x{}-^DH|Yh!w1qPSp`RY&1)-9I`3 zRgx7LoFvm8Nt5*J83O1h3Gow0n1?{zY4qy2dixn?>jjYNnh-~r5|Zi#jU&4g10*+x1(jt(oLZZ&3-! zL8=<46i9JscSI~)yLmrUzStWtgH3Du7HM%nNFT7_@Qwv+Qp5C|W6y8$)<|BQ?p~4l z*waHPK?CIjMe$Y{o}hI@aNmL6bfL!Y^pCjmLg{~gOVvR=8p~Gt3useV%!YsRoh<2| zx{G2!0=0p!G3~gaE`3I zA5spzH~{Mrf1H-v1+X!2VZWeQlbp(>=Y9L&438ke49Zje`)H|(Boj@vWJ zoTd7fLG{0MXu$9|Ey?q9R=3UX;C-f;IKw!8_L~mrJu`Er3w12YvcMO|SIbOy7XVvL zQfMYd=_@5PP!q*rclQ?+_i3=TLIUoTD%&F%5#|{9(MN94)*{2U4hMwa@-4PMTPQ@xS{` zcRWD9eiu#j_pL7-+9j5me|a#I5_s$W^(xVYPn*nB>!_ z5C&=e7klsn_mwvtExDD6B!g~AFL^88Zz}xu=SYiS$mccQh!SYVr_U@KXi)DcB^P3f zxPx1sF9~!)$=DaFtJc(Uw0-9*Oyu$|Ii)qHesw|Wbu>?Or!-(y-?{_w9-T21C*+Jx z_fHU_F?d8n2`~`CQDk|>jjvtHo0|Mb9_6RHJ(i01p-1BLw~irgtc3K!C_Uj))I)|9 z`1#)Ftu%zh1f|-%1SMoA4Q>Nz_D7XjvKAY+oUg2RmA_Z7D8*QS?})-;1n}|%F!E(3 zQh53l&7N~iM00c(%D|wwT*Q&&pJtoVal#$WhNSYy(ffo~;;TFvDgUQnt(g{=(vAfZ zRDbr_Nsn*c_f_soR0q|SA`mak%+fzmn{9i0gHM%)F%YV;jvZ+a~kP3g3DxIB~IZHeiSBq9(k8}pC%9`M0 z#{xVv`3RtY+TW4bqHYdQAQ@Ka3{gO7-2x3f9{U=6L~x668CdhiptmNY^3Zp+lkj@{ z@9WNqrk=^>;J8elJUIj&CdJ`QxyjD+Vqd;RJYWv;*{aj;h9RsnwYOF)hBVY*SF^m* zaOUoU~)+oUuVyiI>Y$yo`Y9z4T5Ba==bZw*PZqr^ivS63= zinMBelVrTRvzv@ig~_?>eH5)=UQi&o(kA1lf4$@}h%#oZQ{BBm>$$&2-nEH9AnlKxl)-=QZ|A%Ie38ZuxAI zPa9tYj-4iz^G?;$GSTVvF~WWgT#3LrFIY9B)VY zlKMS@rUY#gLKew4YL7>wp%6W&)CY#&j%0|$b0A%vFrC5Y;WRC{!t6jS3W=z*u`Hc7 z8|n<~rS14**_w*islba5>Bk1|lgOpuRfLD{rtS$*dU_|8Y5#RPaL`^9m@4$a8H!>VPAiupMf)c#vfRm&{K0%vHDKqG#GW-NOl z{5`7AAd;Dd!WXvhqMbm${xuCBUAE&k)BS4x6t|z#zT07LpJcNDs=h~b1A)3MAVhvz z@(eToMA6X9eBAN5^XEP-opo?hEX;*7=5uQr#EcFKlRL}kC!j=Z+ox@ zl9mdJG>!CP=aW5RekHktL2D_g=m>D1Q_4?YS@{D8UfqM}qNh*$>Dsmq4j=1dxza*u z8qWV58FRlD7U^0Zb)Nf~M)hd@r_ErOcPL~7M>tT~0Ni=kRFY0q0t}xNF4H7Lj3D#pM(t`^?N5 zy?E}YenN4HIWC2;zOz2-Ee#Fsf_t2Dov5T~+4Hc^-7y-?^3|b8Qri~Gr{>mf*9;1h z3v4NLjl4ud5KF1@Rr$~v*+G{5HrWL|zO~*aTgtHzDG%Ya&GF}aJ$Y2i`{@_GJ{`It zEl~w$sWrr}jd{mnW;PG5Gi)9nVX@cz+jEH<+alQTd8SJ%XrCNsZa(wmp$XSGqc=w! zZ7u8O)@IS36Fm>xd>e$cKhWt$Jg~0To2CM`BDdv2S|a^_T40|{J3c?{FQ%lpdueV-+C4ZJ>l_yDjVphY z4*s(~aB4L0tiVqm6;GI816d)r_LnGnYe`%L@*|OTLuq;>FH5euOn-D(WY`Tc1V2LW zv;HXweiF$4(g3j%Vj)~+^fmR&9(q(;si3+4Xx!4rStdlj1&Edg z)k~+Q+-DnDHxy!gzP2`>1gi2*vJyC%930odL>3#Q`h!V7Ld`D*xY7&`UEC&O!YGiVcVW9N=uda^nnt5Cs zdaP;F4^fGWJQpyN$zo)!1WGws-e8;8`=vRN?J}2}rj9LE`oP-cvy;7`7IAcO33I_0 z_J+MsuOW65nSYiXQk$Ouwfo&u?D7$~Jr7MtKj{f`#KeRYTf^|jvB8eh zD4|6zAFRCj;o+0gv$W=uSga@cA)!mQVnmM*OTt7o5KejsA&R3W@%ZDgN>pV+40q<( zg|y{vMqpFyr^v?Od09k4X7$R$sH*|i?KGCXN4qe(h*b`D&OxjWHYHgyUfXm*!Dhi98Y)1KKy^D^z>KIR z$kf+$>FM@&bbkuHte&~d#TD3gcnS7Q*_!wj(l%ZhUV`uGbUKKwv;ORmRzQn2mu(FW z2ykV^1L^z)dn78`hH89Nu|hdjpq{7J9x=+kuRBWR?l$0hh-+SEPV~9ioGIz?eV0M! zp852vyd}^+3l&TjW*gS4^7B<*PnK4H=<8YDA^ElrRLOmhpbtyPCLBqSTtyt~3DM^x zo@}UfGjRgOXtZhm`!^??8!`J89g}T(9}I}6F7PrbElKz}XuCyQ#H`)&<8ZhztlTqQ z!BRPX{OtB4UmxVv)EuE+WZ1DL=3Z@c8r`ZWAE6-hx$wrfa2_u!Cnx7E=*2<+LxhNe zO-;u99B1HeNzkOL6>jU0AM%Pj5Tz*~!x!bt#(wPa#pJ}e5kPe^^xzHi209@3J;O|T z>ASV{5|(f=YeWs%z1Mm^bc7Tn(j5JN>V=hNX5WS198}4Y`wBYxFfDsq$&J84=2WO5 z06p>7cLjA8Si>Ywj`871f)JSj-xteVe}!3z%pSSNdK0G;i$(O=uSNosAG4+dWq=~f z9QX^#uguTCPVUfRGv=*Ac5Z<;F-!-VFSvKFF4x^@BkJvXthY7*Du zvHR**KM(5ZKIOHwqDU)pyeR-zCja}9Hy2=fz5IF6c;e=(r1MCVTJXIDxk4p5krV@Eu`#aiV-(a)N)s-;VZs#inmen1MtIBZA3_mSrL z=cgNZ+0)lIQ}MhmI0}e#YYUzBh4gJHRKltSa;`Pwz`|6g{rXK`#`co-OLo7k`@@;0 z!q>M|xtV-~Tb6M#WHxy$F}S~77ero$hVQMpYcr`t<91$-ehY(0P0RY5s!Lq;c@s~{tF?=wsH|>SGIZJa#6b@%j4_FpKptq&Bd@8uZ zj`W(d#KPhiU|jpl!Wiv;Wng$q>6Uy^0pWuuKX(D@q~s{2ScqG~MWI z6DYy(nk7vO)LkrIFyzGzW4pD8Gt3EMB-EtCrn*iF68iF1e;I zZ+=)-JzQ5tUAscEwtaw9@$~7%_kct|ojQJwX?*tLX5L%<$;us1o|VELc<_G`*&0f* z9zFA?2&%wej2n4EaXU5U9{s)X+PiesGI>Ye#iO9rpm;i@hJYNu?c2V3I&8;OyZmM6 z&qFn5ce(a|>M;zT`{!Mo`D0A&?Uxqb@S5>Lk7{~kHMsn8Bb<#zmf6)aJxmIwHf}?K zHHV|^rq36tpcQb2K0_F!B`E6L2H(-2%V?~>|o z*!cXWXtYaAwKY|-BW>xn<};mwmt`fvyk^c{UEGp$=RL9S>dBVRFD=Q;&rcOa4iAcucJ!&1 zHvmSS#sCBpD%zP8;0>FO{D@D{Pw@ydj^2+an6bPAppu`6;qy3{3>9SjTl`My{?W5F zTtUUD*;|b^>YEa1U8tc7?IO&^bGc?mAndZw(RYxW7hqkvXS6ro8SR6@7PnkLpozB~ zuC(BO*?-!3Vo6t65%MY)RvaP1(O-M>LVAy$z^Nplg+Jyr6%;tMNc{x$Qy|UAhUXzG z#tIg)JD7(fg-0W1tZ;Oyn6$s}PE8?22;04k2;QhgF{A^%2XoaDep=uhnapt=^gE^< zE`vZ4KpJj|R(w41?|&JofM3tu%WIvM@adStUsVp)1i+D^|A?@3a?(H^!nXI^!hhpd z``?E6nii5p8{aQ!9s@QZv}9bI2-#ZY+E!DL+x{F>(}LsTD34s$%eHolY}#}4ua(hg0@MmRdhbuMrzVSb|^Ti1O diff --git a/docs/images/sparv_detailed.png b/docs/images/sparv_detailed.png new file mode 100644 index 0000000000000000000000000000000000000000..930a303aa8a8d6df45f5097ea9d75dd9ae142dd2 GIT binary patch literal 9243 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�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 Hd-?wWScCEs literal 0 HcmV?d00001 diff --git a/docs/md2pdf/make_pdf.sh b/docs/md2pdf/make_pdf.sh index 67afe419..205f9e38 100755 --- a/docs/md2pdf/make_pdf.sh +++ b/docs/md2pdf/make_pdf.sh @@ -61,7 +61,7 @@ author: | | | | - | ![](../images/sparv.png){width=2.5cm} + | ![](../images/sparv_detailed.png){width=2.5cm} --- " From 21831b29d8cf0d6250196306bcd7a4eea124285c Mon Sep 17 00:00:00 2001 From: Anne Schumacher Date: Thu, 9 Nov 2023 13:31:03 +0100 Subject: [PATCH 162/175] change tag line on documentation cover --- docs/docsify/_coverpage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docsify/_coverpage.md b/docs/docsify/_coverpage.md index 2d235052..899e503a 100644 --- a/docs/docsify/_coverpage.md +++ b/docs/docsify/_coverpage.md @@ -2,7 +2,7 @@ # Sparv Pipeline Documentation -> Språkbanken's text analysis tool +> Språkbanken's analysis platform

version 5.1.1dev0

From b8d3fd57adbaad1e667537d2817e4d87cc702bff Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Fri, 17 Nov 2023 18:00:22 +0100 Subject: [PATCH 163/175] Switch to using importlib instead of pkg_resources in run_snake.py ... to prevent deprecation warning from messing up the progressbar --- sparv/core/run_snake.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sparv/core/run_snake.py b/sparv/core/run_snake.py index 5c8fa8df..038cc9e6 100644 --- a/sparv/core/run_snake.py +++ b/sparv/core/run_snake.py @@ -5,7 +5,7 @@ import sys import traceback -from pkg_resources import iter_entry_points +from importlib.metadata import entry_points from sparv.core import io, log_handler, paths from sparv.core import registry @@ -16,7 +16,7 @@ # The snakemake variable is provided automatically by Snakemake; the below is just to please the IDE try: - snakemake + snakemake # noqa except NameError: from snakemake.script import Snakemake snakemake: Snakemake @@ -96,7 +96,7 @@ def flush(): module = importlib.import_module(".".join((modules_path, module_name))) except ModuleNotFoundError: # Try to find plugin module - entry_points = dict((e.name, e) for e in iter_entry_points(f"sparv.{plugin_name}")) + entry_points = dict((e.name, e) for e in entry_points(group=f"sparv.{plugin_name}")) entry_point = entry_points.get(module_name) if entry_point: module = entry_point.load() From e0b780d87775a3562f967e0c96a765990c91a476 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Thu, 23 Nov 2023 13:48:08 +0100 Subject: [PATCH 164/175] Switch to using importlib instead of pkg_resources in registry.py --- pyproject.toml | 1 + sparv/core/registry.py | 30 +++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e31e4aae..c55aa0bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "docx2python==1.27.1", "jsonschema==4.19.0", "nltk==3.8.1", + "packaging>=21.0", "pdfplumber==0.9.0", "protobuf~=3.19.0", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/161 "pycountry==22.3.5", diff --git a/sparv/core/registry.py b/sparv/core/registry.py index deedcfa3..4ad3d970 100644 --- a/sparv/core/registry.py +++ b/sparv/core/registry.py @@ -107,7 +107,10 @@ def find_modules(no_import: bool = False, find_custom: bool = False) -> list: Returns: A list of available module names. """ - from pkg_resources import iter_entry_points, VersionConflict + from importlib.metadata import entry_points + from packaging.requirements import Requirement + + from sparv import __version__ as sparv_version modules_full_path = paths.sparv_path / paths.modules_dir core_modules_full_path = paths.sparv_path / paths.core_modules_dir @@ -143,12 +146,29 @@ def find_modules(no_import: bool = False, find_custom: bool = False) -> list: add_module_to_registry(m, module_name) # Search for installed plugins - for entry_point in iter_entry_points("sparv.plugin"): + for entry_point in entry_points(group="sparv.plugin"): + skip = False try: m = entry_point.load() - except VersionConflict as e: - console.print(f"[red]:warning-emoji: The plugin {entry_point.dist} could not be loaded. " - f"It requires {e.req}, but the current installed version is {e.dist}.\n") + # Check compatibility with Sparv version + for requirement in entry_point.dist.requires: + req = Requirement(requirement) + if req.name == "sparv-pipeline": + if not sparv_version in req.specifier: + console.print( + f"[red]:warning-emoji: The plugin {entry_point.name} ({entry_point.dist.name}) could not " + f"be loaded. It requires Sparv version {req.specifier}, but the currently running Sparv " + f"version is {sparv_version}.\n" + ) + skip = True + break + if skip: + continue + except Exception as e: + console.print( + f"[red]:warning-emoji: The plugin {entry_point.name} ({entry_point.dist.name}) could not be loaded:\n" + f"\n {e}" + ) continue add_module_to_registry(m, entry_point.name) module_names.append(entry_point.name) From 06193c56157c29d49698ccbbce21cc6472c45e1e Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Thu, 23 Nov 2023 13:48:54 +0100 Subject: [PATCH 165/175] Add TODO regarding pkg_resources in setup.py --- sparv/core/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sparv/core/setup.py b/sparv/core/setup.py index 594bebb8..bff6a1ff 100644 --- a/sparv/core/setup.py +++ b/sparv/core/setup.py @@ -34,6 +34,7 @@ def check_sparv_version() -> Optional[bool]: def copy_resource_files(data_dir: pathlib.Path): """Copy resource files to data dir.""" + # TODO: Use importlib.resources.files instead once we require Python 3.9 resources_dir = pathlib.Path(pkg_resources.resource_filename("sparv", "resources")) for f in resources_dir.rglob("*"): From 2046a35e5fcb848e49f6a3641085783da7a9bb41 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Fri, 24 Nov 2023 12:01:47 +0100 Subject: [PATCH 166/175] Print friendlier error message when user interrupts a module --- sparv/core/run_snake.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sparv/core/run_snake.py b/sparv/core/run_snake.py index 038cc9e6..dc1bf90f 100644 --- a/sparv/core/run_snake.py +++ b/sparv/core/run_snake.py @@ -133,6 +133,8 @@ def flush(): registry.modules[module_name].functions[f_name]["function"](**parameters) if snakemake.params.export_dirs: logger.export_dirs(snakemake.params.export_dirs) + except KeyboardInterrupt as e: + exit_with_error_message("Execution was terminated by an interrupt signal", "sparv.modules." + module_name) except SparvErrorMessage as e: # Any exception raised here would be printed directly to the terminal, due to how Snakemake runs the script. # Instead, we log the error message and exit with a non-zero status to signal to Snakemake that From d3394ef6a32195da50cf56829918559edb4a223e Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Fri, 24 Nov 2023 12:03:49 +0100 Subject: [PATCH 167/175] Print error message when Sparv subprocess is killed --- sparv/core/log_handler.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sparv/core/log_handler.py b/sparv/core/log_handler.py index fe976b88..bd56c2c7 100644 --- a/sparv/core/log_handler.py +++ b/sparv/core/log_handler.py @@ -587,7 +587,16 @@ def missing_annotations_or_files(source, files): # an unexpected exception. Either way, the error message has already been logged, so it doesn't need to # be printed again. self.handled_error = True - + elif "died with Date: Fri, 24 Nov 2023 17:17:50 +0100 Subject: [PATCH 168/175] Add config validation and schema to documentation --- docs/developers-guide/sparv-classes.md | 11 ++++++-- docs/user-manual/corpus-configuration.md | 4 +++ docs/user-manual/running-sparv.md | 35 +++++++++++++----------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/docs/developers-guide/sparv-classes.md b/docs/developers-guide/sparv-classes.md index 66bb3c91..92518c3d 100644 --- a/docs/developers-guide/sparv-classes.md +++ b/docs/developers-guide/sparv-classes.md @@ -183,14 +183,21 @@ the `bin` path inside the Sparv data directory. ## Config -An instance of this class holds a configuration key name and its default value. +An instance of this class holds a configuration key name and its default value. The datatype and values allowed can be +specified, and will be used both for validating the config and when generating the Sparv config JSON schema. **Arguments:** - `name`: The name of the configuration key. - `default`: An optional default value of the configuration key. - `description`: An obligatory description. - +- `datatype`: Typehint specifying the allowed datatype(s). +- `choices`: Iterable with valid choices. +- `pattern`: Regular expression matching valid values (only for the datatype `str`). +- `min`: A `float` representing the minimum numeric value. +- `max`: A `float` representing the maximum numeric value. +- `const`: Restrict the value to a single value. +- `conditions`: List of `Config` objects with conditions that must also be met. ## Corpus An instance of this class holds the name (ID) of the corpus. diff --git a/docs/user-manual/corpus-configuration.md b/docs/user-manual/corpus-configuration.md index 4c94eac1..f2de7345 100644 --- a/docs/user-manual/corpus-configuration.md +++ b/docs/user-manual/corpus-configuration.md @@ -32,6 +32,10 @@ export: > has its own section in the file, like the `metadata` and `export` sections in the example above. > By using the `sparv modules` command you can get a list of the available configuration keys and their descriptions. +## Config Schema +Running `sparv schema` will output a JSON schema which can be used in many text editors to validate your config file as +you are creating it, and in some editors can be used to provide autocompletion. + ## Corpus Config Wizard The corpus config wizard is a tool designed to help you create a corpus config file by asking questions about your corpus and the annotations you would like Sparv to add to it. Run `sparv wizard` in order to start the tool. When diff --git a/docs/user-manual/running-sparv.md b/docs/user-manual/running-sparv.md index 4064f9d9..6cecb546 100644 --- a/docs/user-manual/running-sparv.md +++ b/docs/user-manual/running-sparv.md @@ -18,30 +18,33 @@ corpora](https://github.com/spraakbanken/sparv-pipeline/releases/latest/download When running `sparv` (or `sparv -h`) the available sparv commands will be listed: ``` Annotating a corpus: - run Annotate a corpus and generate export files - install Annotate and install a corpus on remote server - clean Remove output directories + run Annotate a corpus and generate export files + install Install a corpus + uninstall Uninstall a corpus + clean Remove output directories Inspecting corpus details: - config Display the corpus config - files List available corpus source files (input for Sparv) + config Display the corpus config + files List available corpus source files (input for Sparv) Show annotation info: - modules List available modules and annotations - presets List available annotation presets - classes List available annotation classes - languages List supported languages + modules List available modules and annotations + presets List available annotation presets + classes List available annotation classes + languages List supported languages Setting up the Sparv Pipeline: - setup Set up the Sparv data directory - wizard Run config wizard to create a corpus config - build-models Download and build the Sparv models (optional) + setup Set up the Sparv data directory + wizard Run config wizard to create a corpus config + build-models Download and build the Sparv models (optional) Advanced commands: - run-rule Run specified rule(s) for creating annotations - create-file Create specified file(s) - run-module Run annotator module independently - preload Preload annotators and models + run-rule Run specified rule(s) for creating annotations + create-file Create specified file(s) + run-module Run annotator module independently + preload Preload annotators and models + autocomplete Enable tab completion in bash + schema Print a JSON schema for the Sparv config format ``` Every command in the Sparv command line interface has a help text which can be accessed with the `-h` flag. Below we From 123c0d517abcb80e675c311ce1f189e2e5adf154 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Wed, 29 Nov 2023 16:17:36 +0100 Subject: [PATCH 169/175] Use importlib_metadata instead of importlib.metadata for compatibility with Python <3.10 --- pyproject.toml | 1 + sparv/core/registry.py | 2 +- sparv/core/run_snake.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c55aa0bc..fcc08aa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "appdirs==1.4.4", "argcomplete==3.0.5", "docx2python==1.27.1", + "importlib-metadata==6.8.0", # For Python <3.10 compatibility "jsonschema==4.19.0", "nltk==3.8.1", "packaging>=21.0", diff --git a/sparv/core/registry.py b/sparv/core/registry.py index 4ad3d970..f4b24a55 100644 --- a/sparv/core/registry.py +++ b/sparv/core/registry.py @@ -107,7 +107,7 @@ def find_modules(no_import: bool = False, find_custom: bool = False) -> list: Returns: A list of available module names. """ - from importlib.metadata import entry_points + from importlib_metadata import entry_points from packaging.requirements import Requirement from sparv import __version__ as sparv_version diff --git a/sparv/core/run_snake.py b/sparv/core/run_snake.py index dc1bf90f..2af073f2 100644 --- a/sparv/core/run_snake.py +++ b/sparv/core/run_snake.py @@ -5,7 +5,7 @@ import sys import traceback -from importlib.metadata import entry_points +from importlib_metadata import entry_points from sparv.core import io, log_handler, paths from sparv.core import registry From 0bb39826a5a57da0b3e4966fabbc4a135c1180e2 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Wed, 29 Nov 2023 17:13:36 +0100 Subject: [PATCH 170/175] Accept pre-release versions of Sparv when checking plugin compatibility --- sparv/core/registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sparv/core/registry.py b/sparv/core/registry.py index f4b24a55..b8699475 100644 --- a/sparv/core/registry.py +++ b/sparv/core/registry.py @@ -154,6 +154,7 @@ def find_modules(no_import: bool = False, find_custom: bool = False) -> list: for requirement in entry_point.dist.requires: req = Requirement(requirement) if req.name == "sparv-pipeline": + req.specifier.prereleases = True # Accept pre-release versions of Sparv if not sparv_version in req.specifier: console.print( f"[red]:warning-emoji: The plugin {entry_point.name} ({entry_point.dist.name}) could not " From 723695a48b74ccd2c3efb66b2659abef3bd18bf7 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Mon, 4 Dec 2023 14:45:01 +0100 Subject: [PATCH 171/175] Bump protobuf dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fcc08aa0..f47be19f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "nltk==3.8.1", "packaging>=21.0", "pdfplumber==0.9.0", - "protobuf~=3.19.0", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/161 + "protobuf>=3.19.0,<4.0.0", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/161 "pycountry==22.3.5", "python-dateutil==2.8.2", "python-json-logger==2.0.7", From eee406585306a64e9ed219802d7dd12d351aadb4 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Mon, 4 Dec 2023 14:48:27 +0100 Subject: [PATCH 172/175] Format pyproject.toml --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f47be19f..90460f5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,12 +16,12 @@ dependencies = [ "appdirs==1.4.4", "argcomplete==3.0.5", "docx2python==1.27.1", - "importlib-metadata==6.8.0", # For Python <3.10 compatibility + "importlib-metadata==6.8.0", # For Python <3.10 compatibility "jsonschema==4.19.0", "nltk==3.8.1", "packaging>=21.0", "pdfplumber==0.9.0", - "protobuf>=3.19.0,<4.0.0", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/161 + "protobuf>=3.19.0,<4.0.0", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/161 "pycountry==22.3.5", "python-dateutil==2.8.2", "python-json-logger==2.0.7", @@ -30,7 +30,7 @@ dependencies = [ "rich==13.3.3", "snakemake==7.25.0", "stanza==1.5.1", - "torch>=1.9.1", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/82 + "torch>=1.9.1", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/82 "typing-inspect==0.8.0", ] @@ -51,7 +51,7 @@ sparv = "sparv.__main__:main" [tool.hatch] version.path = "sparv/__init__.py" build.include = ["/sparv"] -publish.index.disable = true # Require confirmation to publish +publish.index.disable = true # Require confirmation to publish [tool.black] line-length = 120 From 24500aac00a723637fa03b126c06f83fe5e810ae Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Wed, 6 Dec 2023 14:04:51 +0100 Subject: [PATCH 173/175] Upgrade Snakemake --- pyproject.toml | 2 +- sparv/core/Snakefile | 6 +++--- sparv/core/log_handler.py | 2 +- sparv/core/snake_utils.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 90460f5a..e7353643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "PyYAML==6.0", "questionary==1.10.0", "rich==13.3.3", - "snakemake==7.25.0", + "snakemake==7.32.3", "stanza==1.5.1", "torch>=1.9.1", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/82 "typing-inspect==0.8.0", diff --git a/sparv/core/Snakefile b/sparv/core/Snakefile index e24565d3..8428c89a 100644 --- a/sparv/core/Snakefile +++ b/sparv/core/Snakefile @@ -119,7 +119,7 @@ def make_all_files_rule(rule_storage: snake_utils.RuleStorage) -> None: return # Get Snakemake rule object - sm_rule = getattr(rules, rule_storage.rule_name).rule + sm_rule = workflow.get_rule(rule_storage.rule_name) dependencies = rule_storage.outputs if not rule_storage.abstract else rule_storage.inputs @@ -141,7 +141,7 @@ def make_all_files_rule(rule_storage: snake_utils.RuleStorage) -> None: # Set rule dependencies for every file, so Snakemake knows which rule to use in case of ambiguity. # Converting the values of rule_outputs to snakemake.io.IOFile objects is not enough, since file paths must match # for that to work (which they don't do once we've expanded the {file} wildcard). - this_sm_rule = getattr(rules, rule_storage.target_name).rule + this_sm_rule = workflow.get_rule(rule_storage.target_name) for f in this_sm_rule.input: this_sm_rule.dependencies[f] = sm_rule @@ -441,7 +441,7 @@ rule list_uninstalls: # Rule for making exports defined in corpus config if "export_corpus" in selected_targets: - export_targets = snake_utils.get_export_targets(snake_storage, rules, + export_targets = snake_utils.get_export_targets(snake_storage, workflow, file=snake_utils.get_file_values(config, snake_storage), wildcards=snake_utils.get_wildcard_values(config)) rule export_corpus: diff --git a/sparv/core/log_handler.py b/sparv/core/log_handler.py index bd56c2c7..9a609100 100644 --- a/sparv/core/log_handler.py +++ b/sparv/core/log_handler.py @@ -501,7 +501,7 @@ def missing_annotations_or_files(source, files): lines = msg["msg"].splitlines()[3:] total_jobs = lines[-1].split()[1] for j in lines[:-1]: - job, count, _, _ = j.split() + job, count = j.split() if ":" in job and not "::" in job: job = job + "*" # Differentiate entrypoints from actual rules in the list self.jobs[job.replace("::", ":")] = int(count) diff --git a/sparv/core/snake_utils.py b/sparv/core/snake_utils.py index 6920229b..e2320c85 100644 --- a/sparv/core/snake_utils.py +++ b/sparv/core/snake_utils.py @@ -792,7 +792,7 @@ def get_install_outputs(snake_storage: SnakeStorage, install_types: Optional[Lis return install_outputs -def get_export_targets(snake_storage, rules, file, wildcards): +def get_export_targets(snake_storage, workflow: snakemake.Workflow, file, wildcards): """Get export targets from sparv_config.""" all_outputs = [] config_exports = set(sparv_config.get("export.default", [])) @@ -803,7 +803,7 @@ def get_export_targets(snake_storage, rules, file, wildcards): # Get all output files for all source files rule_outputs = expand(rule.outputs if not rule.abstract else rule.inputs, file=file, **wildcards) # Get Snakemake rule object - sm_rule = getattr(rules, rule.rule_name).rule + sm_rule = workflow.get_rule(rule.rule_name) all_outputs.append((sm_rule if not rule.abstract else None, rule_outputs)) if config_exports: From be05a88907afa8fc7ff468bb084730ede4ef3bfe Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Wed, 6 Dec 2023 14:15:54 +0100 Subject: [PATCH 174/175] Upgrade dependencies --- pyproject.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7353643..80fa77a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,24 +14,24 @@ authors = [ ] dependencies = [ "appdirs==1.4.4", - "argcomplete==3.0.5", + "argcomplete==3.1.6", "docx2python==1.27.1", "importlib-metadata==6.8.0", # For Python <3.10 compatibility - "jsonschema==4.19.0", + "jsonschema==4.20.0", "nltk==3.8.1", "packaging>=21.0", - "pdfplumber==0.9.0", + "pdfplumber==0.10.3", "protobuf>=3.19.0,<4.0.0", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/161 "pycountry==22.3.5", "python-dateutil==2.8.2", "python-json-logger==2.0.7", - "PyYAML==6.0", + "PyYAML==6.0.1", "questionary==1.10.0", - "rich==13.3.3", + "rich==13.7.0", "snakemake==7.32.3", "stanza==1.5.1", "torch>=1.9.1", # Used by Stanza; see https://github.com/spraakbanken/sparv-pipeline/issues/82 - "typing-inspect==0.8.0", + "typing-inspect==0.9.0", ] [project.optional-dependencies] From be1432251e4de1d23e20c9b2ba7dfa4e6d38fa97 Mon Sep 17 00:00:00 2001 From: Martin Hammarstedt Date: Thu, 7 Dec 2023 15:57:06 +0100 Subject: [PATCH 175/175] Update changelog and bump version number --- CHANGELOG.md | 6 +++++- docs/docsify/_coverpage.md | 2 +- sparv/__init__.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c646dd07..8a6d5bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # Changelog -## [5.2.0.dev0] +## [5.2.0] - 2023-12-07 ### Added - Added support for tab autocompletion in bash. +- Added importer for PDF files. - Added new `misc:inherit` annotator for inheriting attributes. - Added `korp.wordpicture_no_sentences` setting to disable generation of Word Picture sentences table. - `util.mysql_wrapper` can now execute SQL queries remotely over SSH. @@ -35,6 +36,7 @@ - Running `sparv schema` will now generate a JSON schema which can be used to validate corpus config files. - More strict config validation, including validation of config values and data types. - Most Sparv decorators now have a `priority` parameter, to control the order in which functions are run. +- Added `util.misc.dump_yaml()` utility function for exporting YAML. ### Changed @@ -59,6 +61,7 @@ - Not specifying a corpus language now excludes all language specific annotators. - When an unhandled exception occurs, the relevant source document will be displayed in the log. - `localhost` as an installation target is no longer handled as if host was omitted. +- Removed `critical` log level. ### Fixed @@ -304,6 +307,7 @@ - Increased independence between modules and language models - This facilitates adding new annotation modules and import/export formats. +[5.2.0]: https://github.com/spraakbanken/sparv-pipeline/releases/tag/v5.2.0 [5.1.0]: https://github.com/spraakbanken/sparv-pipeline/releases/tag/v5.1.0 [5.0.0]: https://github.com/spraakbanken/sparv-pipeline/releases/tag/v5.0.0 [4.1.1]: https://github.com/spraakbanken/sparv-pipeline/releases/tag/v4.1.1 diff --git a/docs/docsify/_coverpage.md b/docs/docsify/_coverpage.md index 899e503a..17f94ee5 100644 --- a/docs/docsify/_coverpage.md +++ b/docs/docsify/_coverpage.md @@ -4,7 +4,7 @@ > Språkbanken's analysis platform -

version 5.1.1dev0

+

version 5.2.0