Skip to content
This repository was archived by the owner on Jun 30, 2024. It is now read-only.

Commit 8034ab8

Browse files
committed
merge bjones updates
2 parents 2e5240e + ab5c86e commit 8034ab8

File tree

9 files changed

+38
-230
lines changed

9 files changed

+38
-230
lines changed

docker/.env.prototype

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
RUNESTONE_HOST=localhost
88

99
# Select a configuration for the instructor-facing server (the Runestone server) and the student-facing server (BookServer). Valid values are ``test``, ``development``, and ``production``.
10-
SERVER_CONFIG=production
11-
10+
SERVER_CONFIG=${SERVER_CONFIG}
1211

1312
# For production, change the credentials for the DB to something more secure.
1413
# This should be done prior to first running ``docker/docker_tools.py up``.
@@ -22,9 +21,9 @@ POSTGRES_HOST=db
2221
#
2322
# An admin password is required to log in to the admin interface when using HTTPS.
2423
WEB2PY_ADMIN_PASSWORD=your_password_here
25-
# To generate a new salt value, run ``cd $WEB2PY_PATH; python -c "from gluon.utils import web2py_uuid; print(f'sha512:{web2py_uuid()}')"`` in Docker, then paste the displayed value here. (Adapted from web2py's ``Auth.get_or_create_key`` in ``gluon/tools.py``; see also `web2py authentication <http://web2py.com/books/default/chapter/29/09/access-control#Authentication>`_.)
24+
# You **must** generate a new salt value; the value here IS NOT SECURE. To generate a new salt value, run ``cd $WEB2PY_PATH; python -c "from gluon.utils import web2py_uuid; print(f'sha512:{web2py_uuid()}')"`` in Docker, then paste the displayed value here. (Adapted from web2py's ``Auth.get_or_create_key`` in ``gluon/tools.py``; see also `web2py authentication <http://web2py.com/books/default/chapter/29/09/access-control#Authentication>`_.)
2625
WEB2PY_SALT=sha512:16492eda-ba33-48d4-8748-98d9bbdf8d33
27-
# To generate a new secret, run ``python -c "import secrets; print(secrets.token_urlsafe(16))"``.
26+
# You **must** generate a new secret; the value here IS NOT SECURE. To generate a new secret, run ``python -c "import secrets; print(secrets.token_urlsafe(16))"``.
2827
JWT_SECRET=WT2epQY9p_7HmGVLyRTw5g
2928
# If you support books that students must pay to access, provide Stripe keys.
3029
# STRIPE_PUBLISHABLE_KEY = pk_live_xxx

docker/README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ For the development use case, you do not need to modify any of the default envir
200200

201201
**OR**
202202

203-
For the production use case, you will need to modify these variables. To do so, edit the ``.env`` file, which Docker will read automatically as it loads containers. A sample ``.env`` file is provided as ``./.env`` (copied from `docker/.env.prototype <.env.prototype>` on the first build). See comments in the file for details. Especially pay attention to the `SERVER_CONFIG` value. It defaults to `development` and you will need to change it to `production` if you do a `build --single` or just `build` defaults to single or `build --multi`.
203+
For the production use case, you will need to modify these variables. To do so, edit the ``.env`` file, which Docker will read automatically as it loads containers. A sample ``.env`` file is provided as ``./.env`` (copied from `docker/.env.prototype <.env.prototype>` on the first build). See comments in the file for details.
204204

205205
Python Settings
206206
^^^^^^^^^^^^^^^

docker/__init__.py

Lines changed: 0 additions & 4 deletions
This file was deleted.

docker/docker_tools.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -533,9 +533,22 @@ def _build_phase_0(
533533
clone_bks = clone_all
534534
clone_rc = clone_all
535535

536-
# Create the ``docker/.env`` if it doesn't already exist. TODO: keep a dict of {file name, checksum} and save as JSON. Use this to detect if a file was hand-edited; if not, we can simply replace it.
537-
if not Path(".env").is_file():
538-
xqt("cp docker/.env.prototype .env")
536+
# Create ``docker/.env`` if it doesn't already exist or wasn't edited.
537+
dot_env = Path(".env")
538+
if not dot_env.is_file() or not subprocess.run(["md5sum", "--check", ".env.md5"]).returncode:
539+
# Edit the prototype file, providing the correct value of ``SERVER_CONFIG``.
540+
dot_env.write_text(
541+
replace_vars(
542+
Path("docker/.env.prototype").read_text(),
543+
dict(
544+
SERVER_CONFIG="development"
545+
if build_config.is_dev()
546+
else "production"
547+
),
548+
)
549+
)
550+
# Save a checksum, so we can auto-update this if no hand edits were made.
551+
xqt("md5sum .env > .env.md5")
539552

540553
# Do the same for ``1.py``.
541554
one_py = Path("models/1.py")
@@ -850,6 +863,7 @@ def _build_phase_1(
850863
# ^^^^^^^^^^^^^^^^^^^
851864
xqt(
852865
"mkdir -p $WEB2PY_PATH/logs",
866+
"mkdir -p $RUNESTONE_PATH/errors",
853867
"cp $RUNESTONE_PATH/docker/wsgihandler.py $WEB2PY_PATH/wsgihandler.py",
854868
# Set up nginx (partially -- more in step 3 below).
855869
"rm /etc/nginx/sites-enabled/default",
@@ -867,15 +881,18 @@ def _build_phase_1(
867881
"cp $RUNESTONE_PATH/docker/routes.py $WEB2PY_PATH",
868882
# ``sphinxcontrib.paverutils.run_sphinx`` lacks venv support -- it doesn't use ``sys.executable``, so it doesn't find ``sphinx-build`` in the system path when executing ``/srv/venv/bin/runestone`` directly, instead of activating the venv first (where it does work). As a huge, ugly hack, symlink it to make it available in the system path.
869883
"ln -sf $RUNESTONE_PATH/.venv/bin/sphinx-build /usr/local/bin",
870-
# Deal with a different subdirectory layout inside the container (mandated by web2py) and outside the container by adding these symlinks.
871-
# TODO: should only do this in dev
872-
"ln -sf $BOOK_SERVER_PATH $WEB2PY_PATH/applications/BookServer",
873-
# We can't use ``$BOOK_SERVER_PATH`` here, since we need ``/srv/bookserver-dev`` in lowercase, not CamelCase.
874-
"ln -sf /srv/bookserver-dev $WEB2PY_PATH/applications/bookserver-dev",
875-
"ln -sf /srv/RunestoneComponents $WEB2PY_PATH/applications/RunestoneComponents",
876-
"ln -sf /srv/runestone-dev $WEB2PY_PATH/applications/runestone-dev",
877884
)
878885

886+
if build_config.is_dev():
887+
xqt(
888+
# Deal with a different subdirectory layout inside the container (mandated by web2py) and outside the container by adding these symlinks.
889+
"ln -sf $BOOK_SERVER_PATH $WEB2PY_PATH/applications/BookServer",
890+
# We can't use ``$BOOK_SERVER_PATH`` here, since we need ``/srv/bookserver-dev`` in lowercase, not CamelCase.
891+
"ln -sf /srv/bookserver-dev $WEB2PY_PATH/applications/bookserver-dev",
892+
"ln -sf /srv/RunestoneComponents $WEB2PY_PATH/applications/RunestoneComponents",
893+
"ln -sf /srv/runestone-dev $WEB2PY_PATH/applications/runestone-dev",
894+
)
895+
879896
# Record info about this build. We can't provide ``git`` info, since the repo isn't available (the ``${RUNSTONE_PATH}.git`` directory is hidden, so it's not present at this time). Likewise, volumes aren't mapped, so ``git`` info for the Runestone Components and BookServer isn't available.
880897
Path("/srv/build_info.txt").write_text(
881898
f"Built on {datetime.datetime.now(datetime.timezone.utc)} using arguments {env.DOCKER_BUILD_ARGS}.\n"
@@ -1076,7 +1093,7 @@ def _build_phase_2_core(
10761093

10771094
# Utilities
10781095
# =========
1079-
# A utility to replace all instances of ``${var_name}`` in a string, where the variables are provided in ``vars_``. This is an alternative to the build-in ``str.format()`` which doesn't require escaping all the curly braces.
1096+
# A utility to replace all instances of ``${var_name}`` in a string, where the variables are provided in ``vars_``. This is an alternative to the built-in ``str.format()`` which doesn't require escaping all the curly braces.
10801097
def replace_vars(str_: str, vars_: Dict[str, str]) -> str:
10811098
def repl(matchobj: re.Match):
10821099
var_name = matchobj.group(1)
@@ -1098,7 +1115,6 @@ def run_poetry(is_dev: bool):
10981115
no_dev_arg = "" if is_dev else " --no-dev"
10991116
xqt(
11001117
# Update dependencies. See `scripts/poetry_fix.py`. This must come before Poetry, since it will check for the existence of the project created by these commands. (Even calling ``poetry config`` will perform this check!)
1101-
f"{sys.executable} -m pip install --user toml",
11021118
f"{sys.executable} runestone_poetry_project/poetry_fix.py{no_dev_arg}",
11031119
# By default, Poetry creates a venv in the home directory of the current user (root). However, this isn't accessible when running as ``www-data``. So, tell it to create the venv in a `subdirectory of the project <https://python-poetry.org/docs/configuration/#virtualenvsin-project>`_ instead, which is accessible and at a known location (``./.venv``).
11041120
"poetry config virtualenvs.in-project true",

docker/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
name="runestone-docker-tools",
88
version="0.1",
99
install_requires=["click"],
10-
py_modules=["docker_tools"],
10+
py_modules=["docker_tools", "docker_tools_misc"],
1111
entry_points={"console_scripts": ["docker-tools = docker_tools:cli"]},
1212
)

pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ altair = "^4.0.0"
3131
beautifulsoup4 = "^4.0.0"
3232
bleach = "> 3.1.1"
3333
bookserver = ">= 1.3.3"
34+
boto3 = "^1.26.30"
3435
cssselect = ">= 1.0"
3536
diff-match-patch = ">= 20110725.1"
3637
lxml = ">= 4.6.2"
@@ -60,11 +61,9 @@ pretext = "^1.0.0"
6061

6162
# Development dependencies
6263
# ========================
63-
boto3 = "^1.26.30"
6464
[tool.poetry.dev-dependencies]
6565
black = "~= 22.0"
6666
bookserver = { path = "../BookServer", develop = true }
67-
bookserver-dev = { path = "../bookserver-dev", develop = true }
6867
CodeChat = "^1.0.0"
6968
contextlib2 = "^0.6.0"
7069
coverage = "^6.0.0"
@@ -81,7 +80,6 @@ pytest = "^7.0.0"
8180
pyvirtualdisplay = "^3.0.0"
8281
pywin32 = { version = ">= 301", markers = "sys.platform == 'win32'" }
8382
runestone = { path = "../RunestoneComponents", develop = true }
84-
runestone-dev = { path = "../runestone-dev", develop = true }
8583
selenium = "^3.0.0"
8684

8785

rsmanage/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
install_requires=["click"],
88
entry_points="""
99
[console_scripts]
10-
rsmanage=rsmanage:cli
10+
rsmanage=rsmanage.rsmanage:cli
1111
""",
1212
)

runestone_poetry_project/poetry_fix.py

Lines changed: 2 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -27,86 +27,7 @@
2727
# bookserver = { path = "../BookServer", develop = true }
2828
# runestone = { path = "../RunestoneComponents", develop = true }
2929
#
30-
# ...in production mode; it does the opposite (changes ``[tool.poetry.dependencies]`` to ``[tool.no-poetry.dependencies]``) in development mode. This hides the modified section from Poetry, so the file now looks like an either/or project.
31-
#
32-
# #. Poetry doesn't install development dependencies in projects included through a `path dependency <https://python-poetry.org/docs/dependency-specification/#path-dependencies>`_. As a workaround, this script copies development dependencies from a project into an otherwise empty, auto-created "project", but puts them in the production dependencies section of this newly-created "project", so they will be installed. For example, the BookServer ``pyproject.toml`` contains:
33-
#
34-
# .. code-block:: text
35-
#
36-
# [tool.poetry.dev-dependencies]
37-
# black = "~= 22.0"
38-
# console-ctrl = "^0.1.0"
39-
# ...many more, which are omitted for clarity...
40-
#
41-
# Poetry won't install these. Therefore, `make_dev_pyproject <make_dev_pyproject>` creates a "project" named bookserver-dev whose ``pyproject.toml`` contains a copy of the BookServer development dependencies, but placed in the production dependencies section of this ``bookserver-dev`` "project", so they will be installed. For example, the bookserver-dev ``pyproject.toml`` contains:
42-
#
43-
# .. code-block:: text
44-
#
45-
# [tool.poetry.dependencies] # <== CHANGED!
46-
# black = "~= 22.0"
47-
# console-ctrl = "^0.1.0"
48-
# ...many more, which are omitted for clarity...
49-
#
50-
# This also means that the RunestoneServer ``pyproject.toml`` file must be manually edited to include a reference to this "project":
51-
#
52-
# .. code-block:: text
53-
#
54-
# [tool.poetry.dev-dependencies]
55-
# bookserver = { path = "../BookServer", develop = true }
56-
# bookserver-dev = { path = "../bookserver-dev", develop = true } # <== MANUALLY ADDED!
57-
#
58-
# The final result looks like this:
59-
#
60-
# .. image:: poetry_fix_diagram.png
61-
#
62-
# #. Poetry generates invalid package metadata for local path dependencies, so that running ``pip show click`` results in a bunch of exceptions. This program doesn't provide a fix for this bug.
63-
#
64-
# ...and that's how using Poetry makes dependency management easier...
65-
#
66-
#
67-
# `Invalid package METADATA <https://github.com/python-poetry/poetry/issues/3148>`_
68-
# =====================================================================================
69-
# Per the issue linked in the title above, Poetry generates invalid package metadata for local path dependencies (tested on Poetry v1.1.14). For example, the last few lines of ``.venv/lib/python3.8/site-packages/runestone_poetry_project-0.1.0.dist-info/METADATA`` contain:
70-
#
71-
# .. code-block:: text
72-
#
73-
# Requires-Dist: pytz (>=2016.6.1)
74-
# Requires-Dist: requests (>=2.10.0)
75-
# Requires-Dist: rsmanage @ rsmanage
76-
# Requires-Dist: runestone
77-
# Requires-Dist: runestone-docker-tools @ docker
78-
# Requires-Dist: six (>=1.10.0)
79-
# Requires-Dist: sphinxcontrib-paverutils (>=1.17)
80-
# Requires-Dist: stripe (>=2.0.0,<3.0.0)
81-
#
82-
# This causes an exception when running a command such as ``pip show click``:
83-
#
84-
# .. code-block:: text
85-
#
86-
# ERROR: Exception:
87-
# Traceback (most recent call last):
88-
# File "/srv/web2py/applications/runestone/.venv/lib/python3.8/site-packages/pip/_vendor/pkg_resources/__init__.py", line 3021, in _dep_map
89-
# return self.__dep_map
90-
# File "/srv/web2py/applications/runestone/.venv/lib/python3.8/site-packages/pip/_vendor/pkg_resources/__init__.py", line 2815, in __getattr__
91-
# raise AttributeError(attr)
92-
# AttributeError: _DistInfoDistribution__dep_map
93-
#
94-
# ... along with a long traceback of other chained exceptions.
95-
#
96-
# Fixing the ``METADATA`` file to be:
97-
#
98-
# .. code-block:: text
99-
#
100-
# Requires-Dist: pytz (>=2016.6.1)
101-
# Requires-Dist: requests (>=2.10.0)
102-
# Requires-Dist: rsmanage @ file://rsmanage
103-
# Requires-Dist: runestone
104-
# Requires-Dist: runestone-docker-tools @ file://docker
105-
# Requires-Dist: six (>=1.10.0)
106-
# Requires-Dist: sphinxcontrib-paverutils (>=1.17)
107-
# Requires-Dist: stripe (>=2.0.0,<3.0.0)
108-
#
109-
# ... along with a similar fix to the ``METADATA`` for ``bookserver_dev`` allows ``pip`` to run successfully.
30+
# ...in production mode; it does the opposite (changes ``[tool.poetry.dev-dependencies]`` to ``[tool.no-poetry.dev-dependencies]``) in development mode. This hides the modified section from Poetry, so the file now looks like an either/or project.
11031
#
11132
#
11233
# TODO
@@ -121,134 +42,16 @@
12142
# Standard library
12243
# ----------------
12344
from pathlib import Path
124-
import sys
125-
from typing import Any, Dict, Set
12645

12746
# Third-party imports
12847
# -------------------
12948
import click
130-
import toml
13149

13250

13351
# Local application imports
13452
# -------------------------
13553
# None.
13654
#
137-
# Fix for ``dev-dependencies`` in subprojects
138-
# ===========================================
139-
# Given a main Poetry ``pyproject.toml``, these functions look for all subprojects included via path dependencies, creating additional subprojects named ``projectname-dev`` in which the subproject's dev-dependencies become dependencies in the newly-created subproject. This is a workaround for Poetry's inability to install the dev dependencies for a sub project included via a path requirement. To use this, in the main project, do something like:
140-
#
141-
# .. code-block:: TOML
142-
# :linenos:
143-
#
144-
# [tool.poetry.dev-dependencies]
145-
# sub = { path = "../sub", develop = true }
146-
# sub-dev = { path = "../sub-dev", develop = true }
147-
#
148-
# Create a project clone where the original project's dev-dependencies are dependencies in the clone.
149-
def create_dev_dependencies(
150-
# The path to the project.
151-
project_path: Path,
152-
) -> None:
153-
# Create a dev-only flavor.
154-
d = toml.load(project_path / "pyproject.toml")
155-
tp = d["tool"]["poetry"]
156-
dd = "dev-dependencies"
157-
# If there are no dev-dependencies, there's nothing to do. Otherwise, move them to dependencies.
158-
if dd not in tp:
159-
return
160-
tp["dependencies"] = tp.pop(dd)
161-
# Update the project name.
162-
project_name = tp["name"] = tp["name"] + "-dev"
163-
# We don't have a readme -- if it exists, Poetry will complain about the missing file it references. Remove it if it exists.
164-
tp.pop("readme", None)
165-
166-
# Put the output in a ``project_name-dev/`` directory.
167-
dev = project_path.parent / project_name
168-
print(f"Creating {dev}...")
169-
dev.mkdir(exist_ok=True)
170-
(dev / "pyproject.toml").write_text(toml.dumps(d))
171-
172-
# Create a minimal project to make Poetry happy.
173-
project_name = project_name.replace("-", "_")
174-
p = dev / project_name
175-
p.mkdir(exist_ok=True)
176-
(p / "__init__.py").write_text("")
177-
178-
179-
def walk_dependencies(
180-
# A dict of Poetry-specific values.
181-
poetry_dict: Dict[str, Any],
182-
# True to look at dependencies; False to look at dev-dependencies.
183-
is_deps: bool,
184-
# See `project_path`.
185-
project_path: Path,
186-
# See `walked_paths_set`.
187-
walked_paths_set: Set[Path],
188-
# See `poetry_paths_set`.
189-
poetry_paths_set: Set[Path],
190-
):
191-
key = "dependencies" if is_deps else "dev-dependencies"
192-
for dep in poetry_dict.get(key, {}).values():
193-
pth = dep.get("path", "") if isinstance(dep, dict) else None
194-
if pth:
195-
walk_pyproject(project_path / pth, walked_paths_set, poetry_paths_set)
196-
197-
198-
# Given a ``pyproject.toml``, optionally create a dev dependencies project and walk all requirements with path dependencies.
199-
def walk_pyproject(
200-
# The path where a ``pyproject.toml`` exists.
201-
project_path: Path,
202-
# _`walked_paths_set`: a set of Paths already walked.
203-
walked_paths_set: Set[Path],
204-
# _`poetry_paths_set`: a set of Paths that contained a Poetry project. This is a strict subset of walked_paths_set_.
205-
poetry_paths_set: Set[Path],
206-
# True if this is the root ``pyproject.toml`` file -- no dev dependencies will be created for it.
207-
is_root: bool = False,
208-
):
209-
project_path = project_path.resolve()
210-
# Avoid cycles and unnecessary work.
211-
if project_path in walked_paths_set:
212-
return
213-
walked_paths_set.add(project_path)
214-
print(f"Examining {project_path} ...")
215-
216-
# Process dependencies, if this is a Poetry project.
217-
try:
218-
d = toml.load(project_path / "pyproject.toml")
219-
except FileNotFoundError:
220-
return
221-
poetry_paths_set.add(project_path)
222-
tp = d["tool"]["poetry"]
223-
# Search both the dependencies and dev dependencies in this project for path dependencies.
224-
walk_dependencies(tp, True, project_path, walked_paths_set, poetry_paths_set)
225-
walk_dependencies(tp, False, project_path, walked_paths_set, poetry_paths_set)
226-
227-
# (Usually) process this file.
228-
if not is_root:
229-
create_dev_dependencies(project_path)
230-
231-
232-
# .. _make_dev_pyproject:
233-
#
234-
# Core function: run the whole process on the ``pyproject.toml`` in the current directory.
235-
def make_dev_pyproject():
236-
project_paths_set = set()
237-
walk_pyproject(Path("."), set(), project_paths_set, True)
238-
239-
# Check that we processed the BookServer and the RunestoneComponents.
240-
found_bookserver = False
241-
found_runestone_components = False
242-
for path in project_paths_set:
243-
name = path.name
244-
found_bookserver |= name == "BookServer"
245-
found_runestone_components |= name == "RunestoneComponents"
246-
if not found_bookserver:
247-
sys.exit("Error: did not process the BookServer Poetry project.")
248-
if not found_runestone_components:
249-
sys.exit("Error: did not process the RunestoneComponents Poetry project.")
250-
251-
25255
# .. _rename_pyproject:
25356
#
25457
# Workaround for the main ``pyproject.toml``
@@ -306,12 +109,10 @@ def rewrite_pyproject(is_dev: bool) -> None:
306109
)
307110
def main(no_dev: bool):
308111
"""
309-
This script works around Poetry bugs related to path dependencies.
112+
This script works around Poetry limitations to provide support of either/or dependencies.
310113
"""
311114
is_dev = not no_dev
312115
rewrite_pyproject(is_dev)
313-
if is_dev:
314-
make_dev_pyproject()
315116

316117

317118
if __name__ == "__main__":

0 commit comments

Comments
 (0)