plain.dev 0.18.1__tar.gz → 0.19.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {plain_dev-0.18.1 → plain_dev-0.19.0}/LICENSE +14 -0
  2. {plain_dev-0.18.1 → plain_dev-0.19.0}/PKG-INFO +1 -2
  3. plain_dev-0.19.0/plain/dev/__init__.py +4 -0
  4. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/cli.py +32 -4
  5. plain_dev-0.19.0/plain/dev/debug.py +41 -0
  6. plain_dev-0.19.0/plain/dev/entrypoints.py +17 -0
  7. plain_dev-0.19.0/plain/dev/pdb.py +129 -0
  8. {plain_dev-0.18.1 → plain_dev-0.19.0}/pyproject.toml +2 -3
  9. {plain_dev-0.18.1 → plain_dev-0.19.0}/uv.lock +1 -12
  10. plain_dev-0.18.1/plain/dev/__init__.py +0 -5
  11. plain_dev-0.18.1/plain/dev/debug.py +0 -12
  12. plain_dev-0.18.1/plain/dev/entrypoints.py +0 -10
  13. {plain_dev-0.18.1 → plain_dev-0.19.0}/.gitignore +0 -0
  14. {plain_dev-0.18.1 → plain_dev-0.19.0}/README.md +0 -0
  15. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/README.md +0 -0
  16. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/contribute/__init__.py +0 -0
  17. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/contribute/cli.py +0 -0
  18. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/db/__init__.py +0 -0
  19. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/db/cli.py +0 -0
  20. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/db/container.py +0 -0
  21. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/default_settings.py +0 -0
  22. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/gunicorn_logging.json +0 -0
  23. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/mkcert.py +0 -0
  24. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/pid.py +0 -0
  25. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/poncho/__init__.py +0 -0
  26. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/poncho/color.py +0 -0
  27. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/poncho/compat.py +0 -0
  28. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/poncho/manager.py +0 -0
  29. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/poncho/printer.py +0 -0
  30. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/poncho/process.py +0 -0
  31. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/precommit/__init__.py +0 -0
  32. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/precommit/cli.py +0 -0
  33. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/requests.py +0 -0
  34. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/services.py +0 -0
  35. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/templates/dev/requests.html +0 -0
  36. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/urls.py +0 -0
  37. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/utils.py +0 -0
  38. {plain_dev-0.18.1 → plain_dev-0.19.0}/plain/dev/views.py +0 -0
  39. {plain_dev-0.18.1 → plain_dev-0.19.0}/tests/settings.py +0 -0
@@ -51,3 +51,17 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
51
51
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
52
52
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
53
53
  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
54
+
55
+
56
+ ## This package contains code forked from github.com/ionelmc/python-remote-pdb
57
+
58
+ BSD 2-Clause License
59
+
60
+ Copyright (c) 2014-2022, Ionel Cristian Mărieș. All rights reserved.
61
+
62
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
63
+
64
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
65
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
66
+
67
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.dev
3
- Version: 0.18.1
3
+ Version: 0.19.0
4
4
  Summary: Local development tools for Plain.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.11
9
9
  Requires-Dist: click>=8.0.0
10
- Requires-Dist: debugpy~=1.6.3
11
10
  Requires-Dist: gunicorn>20
12
11
  Requires-Dist: plain<1.0.0
13
12
  Requires-Dist: psycopg[binary]~=3.2.2
@@ -0,0 +1,4 @@
1
+ from .cli import cli
2
+ from .requests import RequestsMiddleware
3
+
4
+ __all__ = ["cli", "RequestsMiddleware"]
@@ -4,6 +4,7 @@ import os
4
4
  import platform
5
5
  import subprocess
6
6
  import sys
7
+ import time
7
8
  import tomllib
8
9
  from importlib.metadata import entry_points
9
10
  from importlib.util import find_spec
@@ -65,6 +66,32 @@ def cli(ctx, port, hostname, log_level):
65
66
  sys.exit(returncode)
66
67
 
67
68
 
69
+ @cli.command()
70
+ def debug():
71
+ """Connect to the remote debugger"""
72
+
73
+ def _connect():
74
+ if subprocess.run(["which", "nc"], capture_output=True).returncode == 0:
75
+ return subprocess.run(["nc", "-C", "localhost", "4444"])
76
+ else:
77
+ raise OSError("nc not found")
78
+
79
+ result = _connect()
80
+
81
+ # Try again once without a message
82
+ if result.returncode == 1:
83
+ time.sleep(1)
84
+ result = _connect()
85
+
86
+ # Keep trying...
87
+ while result.returncode == 1:
88
+ click.secho(
89
+ "Failed to connect. Make sure remote pdb is ready. Retrying...", fg="red"
90
+ )
91
+ result = _connect()
92
+ time.sleep(1)
93
+
94
+
68
95
  @cli.command()
69
96
  def services():
70
97
  """Start additional services defined in pyproject.toml"""
@@ -103,6 +130,7 @@ class Dev:
103
130
 
104
131
  self.plain_env = {
105
132
  "PYTHONUNBUFFERED": "true",
133
+ "PLAIN_DEV": "true",
106
134
  "PLAIN_LOG_LEVEL": self.log_level.upper(),
107
135
  "APP_LOG_LEVEL": self.log_level.upper(),
108
136
  **os.environ,
@@ -145,13 +173,13 @@ class Dev:
145
173
  status_bar = Columns(
146
174
  [
147
175
  Text.from_markup(
148
- f"[bold]Tunnel URL[/bold] [underline][link={self.tunnel_url}]{self.tunnel_url}[/link][/underline]"
176
+ f"[bold]Tunnel[/bold] [underline][link={self.tunnel_url}]{self.tunnel_url}[/link][/underline]"
149
177
  ),
150
178
  Text.from_markup(
151
- f"[dim][bold]Localhost URL[/bold] [link={self.url}]{self.url}[/link][/dim]"
179
+ f"[dim][bold]Server[/bold] [link={self.url}]{self.url}[/link][/dim]"
152
180
  ),
153
181
  Text.from_markup(
154
- "[dim]Press [bold]Ctrl+C[/bold] to stop[/dim]",
182
+ "[dim][bold]Ctrl+C[/bold] to stop[/dim]",
155
183
  justify="right",
156
184
  ),
157
185
  ],
@@ -161,7 +189,7 @@ class Dev:
161
189
  status_bar = Columns(
162
190
  [
163
191
  Text.from_markup(
164
- f"[bold]Localhost URL[/bold] [underline][link={self.url}]{self.url}[/link][/underline]"
192
+ f"[bold]Server[/bold] [underline][link={self.url}]{self.url}[/link][/underline]"
165
193
  ),
166
194
  Text.from_markup(
167
195
  "[dim][bold]Ctrl+C[/bold] to stop[/dim]", justify="right"
@@ -0,0 +1,41 @@
1
+ import os
2
+ import platform
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from . import pdb
8
+
9
+
10
+ def set_breakpoint_hook():
11
+ """
12
+ If a `plain dev` process is running, set a
13
+ breakpoint hook to trigger a remote debugger.
14
+ """
15
+
16
+ if not os.environ.get("PLAIN_DEV"):
17
+ # Only want to set the breakpoint hook if
18
+ # we're in a process managed by `plain dev`
19
+ return
20
+
21
+ def _breakpoint():
22
+ system = platform.system()
23
+
24
+ if system == "Darwin":
25
+ pwd = Path.cwd()
26
+ script = f"""
27
+ tell application "Terminal"
28
+ activate
29
+ do script "cd {pwd} && plain dev debug"
30
+ end tell
31
+ """
32
+ subprocess.run(["osascript", "-e", script])
33
+ else:
34
+ raise OSError("Unsupported operating system")
35
+
36
+ pdb.set_trace(
37
+ # Make sure the debugger starts outside of this
38
+ frame=sys._getframe().f_back,
39
+ )
40
+
41
+ sys.breakpointhook = _breakpoint
@@ -0,0 +1,17 @@
1
+ import os
2
+
3
+ from dotenv import load_dotenv
4
+
5
+ from .debug import set_breakpoint_hook
6
+
7
+
8
+ def setup():
9
+ # Try to set a new breakpoint() hook
10
+ # so we can connect to pdb remotely.
11
+ set_breakpoint_hook()
12
+
13
+ # Load environment variables from .env file
14
+ if plain_env := os.environ.get("PLAIN_ENV", ""):
15
+ load_dotenv(f".env.{plain_env}")
16
+ else:
17
+ load_dotenv(".env")
@@ -0,0 +1,129 @@
1
+ import errno
2
+ import logging
3
+ import re
4
+ import socket
5
+ import sys
6
+ from pdb import Pdb
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ def cry(message, stderr=sys.__stderr__):
12
+ log.critical(message)
13
+ print(message, file=stderr)
14
+ stderr.flush()
15
+
16
+
17
+ class LF2CRLF_FileWrapper:
18
+ def __init__(self, connection):
19
+ self.connection = connection
20
+ self.stream = fh = connection.makefile("rw")
21
+ self.read = fh.read
22
+ self.readline = fh.readline
23
+ self.readlines = fh.readlines
24
+ self.close = fh.close
25
+ self.flush = fh.flush
26
+ self.fileno = fh.fileno
27
+ if hasattr(fh, "encoding"):
28
+ self._send = lambda data: connection.sendall(data.encode(fh.encoding))
29
+ else:
30
+ self._send = connection.sendall
31
+
32
+ @property
33
+ def encoding(self):
34
+ return self.stream.encoding
35
+
36
+ def __iter__(self):
37
+ return self.stream.__iter__()
38
+
39
+ def write(self, data, nl_rex=re.compile("\r?\n")):
40
+ data = nl_rex.sub("\r\n", data)
41
+ self._send(data)
42
+
43
+ def writelines(self, lines, nl_rex=re.compile("\r?\n")):
44
+ for line in lines:
45
+ self.write(line, nl_rex)
46
+
47
+
48
+ class DevPdb(Pdb):
49
+ """
50
+ This will run pdb as a ephemeral telnet service. Once you connect no one
51
+ else can connect. On construction this object will block execution till a
52
+ client has connected.
53
+
54
+ Based on https://github.com/tamentis/rpdb I think ...
55
+
56
+ To use this::
57
+
58
+ DevPdb(host='0.0.0.0', port=4444).set_trace()
59
+
60
+ Then run: telnet 127.0.0.1 4444
61
+ """
62
+
63
+ active_instance = None
64
+
65
+ def __init__(self, host, port, patch_stdstreams=False, quiet=False):
66
+ self._quiet = quiet
67
+ listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
68
+ listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
69
+ listen_socket.bind((host, port))
70
+ if not self._quiet:
71
+ cry(
72
+ "DevPdb session open at {}:{}, waiting for connection ...".format(
73
+ *listen_socket.getsockname()
74
+ )
75
+ )
76
+ listen_socket.listen(1)
77
+ connection, address = listen_socket.accept()
78
+ if not self._quiet:
79
+ cry(f"DevPdb accepted connection from {repr(address)}.")
80
+ self.handle = LF2CRLF_FileWrapper(connection)
81
+ Pdb.__init__(self, completekey="tab", stdin=self.handle, stdout=self.handle)
82
+ self.backup = []
83
+ if patch_stdstreams:
84
+ for name in (
85
+ "stderr",
86
+ "stdout",
87
+ "__stderr__",
88
+ "__stdout__",
89
+ "stdin",
90
+ "__stdin__",
91
+ ):
92
+ self.backup.append((name, getattr(sys, name)))
93
+ setattr(sys, name, self.handle)
94
+ DevPdb.active_instance = self
95
+
96
+ def __restore(self):
97
+ if self.backup and not self._quiet:
98
+ cry(f"Restoring streams: {self.backup} ...")
99
+ for name, fh in self.backup:
100
+ setattr(sys, name, fh)
101
+ self.handle.close()
102
+ DevPdb.active_instance = None
103
+
104
+ def do_quit(self, arg):
105
+ self.__restore()
106
+ return Pdb.do_quit(self, arg)
107
+
108
+ do_q = do_exit = do_quit
109
+
110
+ def set_trace(self, frame=None):
111
+ if frame is None:
112
+ frame = sys._getframe().f_back
113
+ try:
114
+ Pdb.set_trace(self, frame)
115
+ except OSError as exc:
116
+ if exc.errno != errno.ECONNRESET:
117
+ raise
118
+
119
+
120
+ def set_trace(
121
+ frame=None, host="127.0.0.1", port=4444, patch_stdstreams=False, quiet=False
122
+ ):
123
+ """
124
+ Opens a remote PDB over a host:port.
125
+ """
126
+ devpdb = DevPdb(
127
+ host=host, port=port, patch_stdstreams=patch_stdstreams, quiet=quiet
128
+ )
129
+ devpdb.set_trace(frame=frame)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.dev"
3
- version = "0.18.1"
3
+ version = "0.19.0"
4
4
  description = "Local development tools for Plain."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  license = "BSD-3-Clause"
@@ -9,7 +9,6 @@ requires-python = ">=3.11"
9
9
  dependencies = [
10
10
  "plain<1.0.0",
11
11
  "click>=8.0.0",
12
- "debugpy~=1.6.3",
13
12
  "python-dotenv~=1.0.0",
14
13
  "gunicorn>20",
15
14
  "requests>=2.0.0",
@@ -24,7 +23,7 @@ dependencies = [
24
23
  "contrib" = "plain.dev.contribute:cli"
25
24
 
26
25
  [project.entry-points."plain.setup"]
27
- "dev-dotenv" = "plain.dev.entrypoints:load_dotenv_file"
26
+ "dev-setup" = "plain.dev.entrypoints:setup"
28
27
 
29
28
  [tool.hatch.build.targets.wheel]
30
29
  packages = ["plain"]
@@ -79,15 +79,6 @@ wheels = [
79
79
  { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
80
80
  ]
81
81
 
82
- [[package]]
83
- name = "debugpy"
84
- version = "1.6.7.post1"
85
- source = { registry = "https://pypi.org/simple" }
86
- sdist = { url = "https://files.pythonhosted.org/packages/0b/c8/b6978e56c917c2fe5096cd83d81bab8ac08ceb1f9a4b9373ad5b066a3265/debugpy-1.6.7.post1.zip", hash = "sha256:fe87ec0182ef624855d05e6ed7e0b7cb1359d2ffa2a925f8ec2d22e98b75d0ca", size = 4664667 }
87
- wheels = [
88
- { url = "https://files.pythonhosted.org/packages/81/72/cb3a101717d81ab1f97ee38a409be9740e7256bc25074e48fd634055261f/debugpy-1.6.7.post1-py2.py3-none-any.whl", hash = "sha256:1093a5c541af079c13ac8c70ab8b24d1d35c8cacb676306cf11e57f699c02926", size = 4872783 },
89
- ]
90
-
91
82
  [[package]]
92
83
  name = "gunicorn"
93
84
  version = "23.0.0"
@@ -214,11 +205,10 @@ wheels = [
214
205
 
215
206
  [[package]]
216
207
  name = "plain-dev"
217
- version = "0.18.1"
208
+ version = "0.19.0"
218
209
  source = { editable = "." }
219
210
  dependencies = [
220
211
  { name = "click" },
221
- { name = "debugpy" },
222
212
  { name = "gunicorn" },
223
213
  { name = "plain" },
224
214
  { name = "psycopg", extra = ["binary"] },
@@ -230,7 +220,6 @@ dependencies = [
230
220
  [package.metadata]
231
221
  requires-dist = [
232
222
  { name = "click", specifier = ">=8.0.0" },
233
- { name = "debugpy", specifier = "~=1.6.3" },
234
223
  { name = "gunicorn", specifier = ">20" },
235
224
  { name = "plain", specifier = "<1.0.0" },
236
225
  { name = "psycopg", extras = ["binary"], specifier = "~=3.2.2" },
@@ -1,5 +0,0 @@
1
- from .cli import cli
2
- from .debug import attach
3
- from .requests import RequestsMiddleware
4
-
5
- __all__ = ["cli", "RequestsMiddleware", "attach"]
@@ -1,12 +0,0 @@
1
- import debugpy
2
-
3
-
4
- def attach(endpoint=("localhost", 5678)):
5
- if debugpy.is_client_connected():
6
- print("Debugger already attached")
7
- return
8
-
9
- debugpy.listen(endpoint)
10
- print("Waiting for debugger to attach...")
11
- debugpy.wait_for_client()
12
- print("Debugger attached!")
@@ -1,10 +0,0 @@
1
- import os
2
-
3
- from dotenv import load_dotenv
4
-
5
-
6
- def load_dotenv_file():
7
- if plain_env := os.environ.get("PLAIN_ENV", ""):
8
- load_dotenv(f".env.{plain_env}")
9
- else:
10
- load_dotenv(".env")
File without changes
File without changes
File without changes
File without changes
File without changes