Skip to content

Commit 400c627

Browse files
authored
Merge branch 'master' into patch-1
2 parents 5013e80 + d4d328f commit 400c627

File tree

17 files changed

+138
-183
lines changed

17 files changed

+138
-183
lines changed

.github/workflows/main.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ on:
55
tags: ['*']
66
pull_request:
77
branches: [master]
8+
workflow_dispatch:
9+
810
permissions:
911
id-token: write
1012
contents: write
@@ -14,7 +16,7 @@ jobs:
1416
matrix:
1517
# TODO: work on windows-latest compatibility?
1618
os: [ubuntu-latest]
17-
python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10']
19+
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
1820
include:
1921
- os: macos-latest
2022
python-version: '3.9'
@@ -28,10 +30,8 @@ jobs:
2830
python-version: ${{ matrix.python-version }}
2931
- run: pip install -r requirements.txt
3032
- run: pip freeze
31-
- if: matrix.python-version > '3.5'
32-
run: make fmt
33-
- if: matrix.python-version >= '3.6'
34-
run: make lint
33+
- run: make fmt
34+
- run: make lint
3535
- run: python setup.py --version
3636
- run: make test-${{ matrix.python-version }}
3737
prerelease-test:

.github/workflows/snyk.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: snyk
2+
on:
3+
schedule:
4+
- cron: "0 10 * * 1" # Monday @ 10am UTC
5+
workflow_dispatch:
6+
7+
env:
8+
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
9+
SNYK_ORG: rstudio-connect
10+
SNYK_PROJECT: rsconnect-python
11+
12+
jobs:
13+
python-dependencies:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@master
17+
- name: Run Snyk on dependencies
18+
uses: snyk/actions/python@master
19+
with:
20+
command: monitor
21+
args: --file=setup.py --print-deps --project-name=${{ env.SNYK_PROJECT }} --org=${{ env.SNYK_ORG }}
22+
python-code:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@master
26+
- name: Run Snyk static analysis
27+
uses: snyk/actions/python@master
28+
with:
29+
command: code test
30+
args: --project-name=${{ env.SNYK_PROJECT }} --org=${{ env.SNYK_ORG }} rsconnect/

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ tooling](https://packaging.python.org/guides/tool-recommendations/).
55

66
To get started, you'll want to:
77
- clone the repo into a project directory
8-
- setup a virtual 3.5+ python environment in the project directory
8+
- setup a virtual 3.7+ python environment in the project directory
99
- activate that virtual environment
1010
- install the dependencies
1111
- validate your build environment with some sample commands

Makefile

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ SOURCE_DATE_EPOCH := $(shell date +%s)
3535
export SOURCE_DATE_EPOCH
3636

3737
.PHONY: all-tests
38-
all-tests: all-images test-3.5 test-3.6 test-3.7 test-3.8 test-3.9 test-3.10
38+
all-tests: all-images test-3.7 test-3.8 test-3.9 test-3.10
3939

4040
.PHONY: all-images
41-
all-images: image-3.5 image-3.6 image-3.7 image-3.8 image-3.9 image-3.10
41+
all-images: image-3.7 image-3.8 image-3.9 image-3.10
4242

4343
image-%:
4444
docker build -t rsconnect-python:$* --build-arg BASE_IMAGE=python:$*-slim .
@@ -62,9 +62,6 @@ mock-test-%: clean-stores
6262
fmt-%:
6363
$(RUNNER) 'black .'
6464

65-
.PHONY: fmt-3.5
66-
fmt-3.5: .fmt-unsupported
67-
6865
.PHONY: .fmt-unsupported
6966
.fmt-unsupported:
7067
@echo ERROR: This python version cannot run the fmting tools
@@ -83,9 +80,6 @@ lint-%:
8380
$(RUNNER) 'flake8 tests/'
8481
$(RUNNER) 'mypy -p rsconnect'
8582

86-
.PHONY: lint-3.5
87-
lint-3.5: .lint-unsupported
88-
8983
.PHONY: .lint-unsupported
9084
.lint-unsupported:
9185
@echo ERROR: This python version cannot run the linting tools

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
!!! warning
44

5-
As of version 1.7.0, rsconnect-python requires Python version 3.5 or higher. Please see the
6-
[official announcement](https://www.rstudio.com/blog/rstudio-connect-2021-08-python-updates/)
7-
for details about this decision.
5+
As of version 1.14.0, rsconnect-python requires Python version 3.7 or higher.
86

97
This package provides a CLI (command-line interface) for interacting
108
with and deploying to Posit Connect. This is also used by the

requirements.txt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
black==22.3.0; python_version >= '3.6'
1+
black==22.3.0
22
click>=7.0.0
33
coverage
44
flake8
@@ -8,13 +8,12 @@ importlib-metadata
88
ipykernel
99
ipython
1010
jupyter_client
11-
mypy; python_version >= '3.6'
11+
mypy
1212
nbconvert
13-
pyjwt>=2.4.0; python_version >= '3.6'
14-
pyjwt; python_version < '3.6'
13+
pyjwt>=2.4.0
1514
pytest
1615
pytest-cov
17-
pytest-mypy; python_version >= '3.5'
16+
pytest-mypy
1817
semver>=2.0.0,<3.0.0
1918
setuptools_scm
2019
six>=1.14.0

rsconnect/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ def validate_app_mode(self, *args, **kwargs):
852852
existing_app_mode = AppModes.get_by_cloud_name(app.json_data["mode"])
853853
else:
854854
raise RSConnectException("Unable to infer Connect client.")
855-
if existing_app_mode and app_mode != existing_app_mode:
855+
if existing_app_mode and existing_app_mode not in (None, AppModes.UNKNOWN, app_mode):
856856
msg = (
857857
"Deploying with mode '%s',\n"
858858
+ "but the existing deployment has mode '%s'.\n"

rsconnect/bundle.py

Lines changed: 68 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pprint import pformat
1515
from collections import defaultdict
1616
from mimetypes import guess_type
17+
from pathlib import Path
1718
import click
1819

1920

@@ -33,7 +34,7 @@
3334

3435
# From https://github.com/rstudio/rsconnect/blob/485e05a26041ab8183a220da7a506c9d3a41f1ff/R/bundle.R#L85-L88
3536
# noinspection SpellCheckingInspection
36-
directories_to_ignore = [
37+
directories_ignore_list = [
3738
".Rproj.user/",
3839
".env/",
3940
".git/",
@@ -47,6 +48,7 @@
4748
"rsconnect/",
4849
"venv/",
4950
]
51+
directories_to_ignore = {Path(d) for d in directories_ignore_list}
5052

5153

5254
# noinspection SpellCheckingInspection
@@ -423,17 +425,20 @@ def make_notebook_html_bundle(
423425
return bundle_file
424426

425427

426-
def keep_manifest_specified_file(relative_path):
428+
def keep_manifest_specified_file(relative_path, ignore_path_set=directories_to_ignore):
427429
"""
428430
A helper to see if the relative path given, which is assumed to have come
429431
from a manifest.json file, should be kept or ignored.
430432
431433
:param relative_path: the relative path name to check.
432434
:return: True, if the path should kept or False, if it should be ignored.
433435
"""
434-
for ignore_me in directories_to_ignore:
435-
if relative_path.startswith(ignore_me):
436+
p = Path(relative_path)
437+
for parent in p.parents:
438+
if parent in ignore_path_set:
436439
return False
440+
if p in ignore_path_set:
441+
return False
437442
return True
438443

439444

@@ -550,56 +555,6 @@ def list_environment_dirs(directory):
550555
return envs
551556

552557

553-
def _create_api_file_list(
554-
directory, # type: str
555-
requirements_file_name, # type: str
556-
extra_files=None, # type: typing.Optional[typing.List[str]]
557-
excludes=None, # type: typing.Optional[typing.List[str]]
558-
):
559-
# type: (...) -> typing.List[str]
560-
"""
561-
Builds a full list of files under the given directory that should be included
562-
in a manifest or bundle. Extra files and excludes are relative to the given
563-
directory and work as you'd expect.
564-
565-
:param directory: the directory to walk for files.
566-
:param requirements_file_name: the name of the requirements file for the current
567-
Python environment.
568-
:param extra_files: a sequence of any extra files to include in the bundle.
569-
:param excludes: a sequence of glob patterns that will exclude matched files.
570-
:return: the list of relevant files, relative to the given directory.
571-
"""
572-
# Don't let these top-level files be added via the extra files list.
573-
extra_files = extra_files or []
574-
skip = [requirements_file_name, "manifest.json"]
575-
extra_files = sorted(list(set(extra_files) - set(skip)))
576-
577-
# Don't include these top-level files.
578-
excludes = list(excludes) if excludes else []
579-
excludes.append("manifest.json")
580-
excludes.append(requirements_file_name)
581-
excludes.extend(list_environment_dirs(directory))
582-
glob_set = create_glob_set(directory, excludes)
583-
584-
file_list = []
585-
586-
for subdir, dirs, files in os.walk(directory):
587-
for file in files:
588-
abs_path = os.path.join(subdir, file)
589-
rel_path = os.path.relpath(abs_path, directory)
590-
591-
if keep_manifest_specified_file(rel_path) and (rel_path in extra_files or not glob_set.matches(abs_path)):
592-
file_list.append(rel_path)
593-
# Don't add extra files more than once.
594-
if rel_path in extra_files:
595-
extra_files.remove(rel_path)
596-
597-
for rel_path in extra_files:
598-
file_list.append(rel_path)
599-
600-
return sorted(file_list)
601-
602-
603558
def make_api_manifest(
604559
directory: str,
605560
entry_point: str,
@@ -624,7 +579,17 @@ def make_api_manifest(
624579
if is_environment_dir(directory):
625580
excludes = list(excludes or []) + ["bin/", "lib/"]
626581

627-
relevant_files = _create_api_file_list(directory, environment.filename, extra_files, excludes)
582+
extra_files = extra_files or []
583+
skip = [environment.filename, "manifest.json"]
584+
extra_files = sorted(list(set(extra_files) - set(skip)))
585+
586+
# Don't include these top-level files.
587+
excludes = list(excludes) if excludes else []
588+
excludes.append("manifest.json")
589+
excludes.append(environment.filename)
590+
excludes.extend(list_environment_dirs(directory))
591+
592+
relevant_files = create_file_list(directory, extra_files, excludes)
628593
manifest = make_source_manifest(app_mode, environment, entry_point, None, image)
629594

630595
manifest_add_buffer(manifest, environment.filename, environment.contents)
@@ -655,6 +620,8 @@ def make_html_bundle_content(
655620
"""
656621
extra_files = list(extra_files) if extra_files else []
657622
entrypoint = entrypoint or infer_entrypoint(path=path, mimetype="text/html")
623+
if not entrypoint:
624+
raise RSConnectException("Unable to find a valid html entry point.")
658625

659626
if path.startswith(os.curdir):
660627
path = relpath(path)
@@ -667,37 +634,15 @@ def make_html_bundle_content(
667634

668635
extra_files = extra_files or []
669636
skip = ["manifest.json"]
670-
extra_files = sorted(list(set(extra_files) - set(skip)))
637+
extra_files = sorted(set(extra_files) - set(skip))
671638

672639
# Don't include these top-level files.
673640
excludes = list(excludes) if excludes else []
674641
excludes.append("manifest.json")
675642
if not isfile(path):
676643
excludes.extend(list_environment_dirs(path))
677-
glob_set = create_glob_set(path, excludes)
678-
679-
file_list = []
680644

681-
for rel_path in extra_files:
682-
file_list.append(rel_path)
683-
684-
if isfile(path):
685-
file_list.append(path)
686-
else:
687-
for subdir, dirs, files in os.walk(path):
688-
for file in files:
689-
abs_path = os.path.join(subdir, file)
690-
rel_path = os.path.relpath(abs_path, path)
691-
692-
if keep_manifest_specified_file(rel_path) and (
693-
rel_path in extra_files or not glob_set.matches(abs_path)
694-
):
695-
file_list.append(rel_path)
696-
# Don't add extra files more than once.
697-
if rel_path in extra_files:
698-
extra_files.remove(rel_path)
699-
700-
relevant_files = sorted(file_list)
645+
relevant_files = create_file_list(path, extra_files, excludes)
701646
manifest = make_html_manifest(entrypoint, image)
702647

703648
for rel_path in relevant_files:
@@ -706,6 +651,48 @@ def make_html_bundle_content(
706651
return manifest, relevant_files
707652

708653

654+
def create_file_list(
655+
path: str,
656+
extra_files: typing.List[str] = None,
657+
excludes: typing.List[str] = None,
658+
) -> typing.List[str]:
659+
"""
660+
Builds a full list of files under the given path that should be included
661+
in a manifest or bundle. Extra files and excludes are relative to the given
662+
directory and work as you'd expect.
663+
664+
:param path: a file, or a directory to walk for files.
665+
:param extra_files: a sequence of any extra files to include in the bundle.
666+
:param excludes: a sequence of glob patterns that will exclude matched files.
667+
:return: the list of relevant files, relative to the given directory.
668+
"""
669+
extra_files = extra_files or []
670+
excludes = excludes if excludes else []
671+
glob_set = create_glob_set(path, excludes)
672+
exclude_paths = {Path(p) for p in excludes}
673+
file_set = set() # type: typing.Set[str]
674+
file_set.union(extra_files)
675+
676+
if isfile(path):
677+
file_set.add(path)
678+
return sorted(file_set)
679+
680+
for subdir, dirs, files in os.walk(path):
681+
if Path(subdir) in exclude_paths:
682+
continue
683+
for file in files:
684+
abs_path = os.path.join(subdir, file)
685+
rel_path = os.path.relpath(abs_path, path)
686+
687+
if Path(abs_path) in exclude_paths:
688+
continue
689+
if keep_manifest_specified_file(rel_path, exclude_paths | directories_to_ignore) and (
690+
rel_path in extra_files or not glob_set.matches(abs_path)
691+
):
692+
file_set.add(rel_path)
693+
return sorted(file_set)
694+
695+
709696
def infer_entrypoint(path, mimetype):
710697
if os.path.isfile(path):
711698
return path
@@ -823,25 +810,9 @@ def _create_quarto_file_list(
823810
excludes = list(excludes) if excludes else []
824811
excludes.append("manifest.json")
825812
excludes.extend(list_environment_dirs(directory))
826-
glob_set = create_glob_set(directory, excludes)
827-
828-
file_list = []
829-
830-
for subdir, dirs, files in os.walk(directory):
831-
for file in files:
832-
abs_path = os.path.join(subdir, file)
833-
rel_path = os.path.relpath(abs_path, directory)
834-
835-
if keep_manifest_specified_file(rel_path) and (rel_path in extra_files or not glob_set.matches(abs_path)):
836-
file_list.append(rel_path)
837-
# Don't add extra files more than once.
838-
if rel_path in extra_files:
839-
extra_files.remove(rel_path)
840-
841-
for rel_path in extra_files:
842-
file_list.append(rel_path)
843813

844-
return sorted(file_list)
814+
file_list = create_file_list(directory, extra_files, excludes)
815+
return file_list
845816

846817

847818
def make_quarto_manifest(

0 commit comments

Comments
 (0)