scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b1__py3-none-any.whl

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 (87) hide show
  1. cli/__about__.py +1 -0
  2. cli/__init__.py +26 -0
  3. cli/cmd/__init__.py +4 -0
  4. cli/cmd/group.py +127 -0
  5. cli/cmd/login.py +60 -0
  6. cli/cmd/profile.py +7 -0
  7. cli/cmd/sessions.py +5 -0
  8. cli/context.py +142 -0
  9. cli/db.py +66 -0
  10. cli/namespace.py +14 -0
  11. {scratchattach/cloud → cloud}/_base.py +112 -87
  12. {scratchattach/cloud → cloud}/cloud.py +16 -16
  13. {scratchattach/editor → editor}/__init__.py +2 -1
  14. {scratchattach/editor → editor}/asset.py +26 -14
  15. {scratchattach/editor → editor}/backpack_json.py +3 -5
  16. {scratchattach/editor → editor}/base.py +2 -4
  17. {scratchattach/editor → editor}/block.py +27 -22
  18. {scratchattach/editor → editor}/blockshape.py +1 -1
  19. {scratchattach/editor → editor}/build_defaulting.py +2 -2
  20. editor/commons.py +145 -0
  21. {scratchattach/editor → editor}/field.py +1 -1
  22. {scratchattach/editor → editor}/inputs.py +6 -3
  23. {scratchattach/editor → editor}/meta.py +10 -7
  24. {scratchattach/editor → editor}/monitor.py +10 -8
  25. {scratchattach/editor → editor}/mutation.py +68 -11
  26. {scratchattach/editor → editor}/pallete.py +1 -3
  27. {scratchattach/editor → editor}/prim.py +4 -0
  28. {scratchattach/editor → editor}/project.py +118 -16
  29. {scratchattach/editor → editor}/sprite.py +25 -15
  30. {scratchattach/editor → editor}/vlb.py +2 -2
  31. {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
  32. {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
  33. {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
  34. {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
  35. {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
  36. {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
  37. eventhandlers/filterbot.py +163 -0
  38. other/other_apis.py +598 -0
  39. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
  40. scratchattach-3.0.0b1.dist-info/RECORD +79 -0
  41. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
  42. scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
  43. scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
  44. {scratchattach/site → site}/_base.py +32 -5
  45. site/activity.py +426 -0
  46. {scratchattach/site → site}/alert.py +4 -5
  47. {scratchattach/site → site}/backpack_asset.py +2 -1
  48. {scratchattach/site → site}/classroom.py +80 -73
  49. {scratchattach/site → site}/cloud_activity.py +43 -29
  50. {scratchattach/site → site}/comment.py +86 -100
  51. {scratchattach/site → site}/forum.py +8 -4
  52. site/placeholder.py +132 -0
  53. {scratchattach/site → site}/project.py +228 -122
  54. {scratchattach/site → site}/session.py +156 -71
  55. {scratchattach/site → site}/studio.py +139 -46
  56. site/typed_dicts.py +151 -0
  57. {scratchattach/site → site}/user.py +511 -215
  58. {scratchattach/utils → utils}/commons.py +12 -4
  59. {scratchattach/utils → utils}/encoder.py +7 -4
  60. {scratchattach/utils → utils}/enums.py +1 -0
  61. {scratchattach/utils → utils}/exceptions.py +36 -2
  62. utils/optional_async.py +154 -0
  63. utils/requests.py +306 -0
  64. scratchattach/__init__.py +0 -29
  65. scratchattach/editor/commons.py +0 -273
  66. scratchattach/eventhandlers/filterbot.py +0 -161
  67. scratchattach/other/other_apis.py +0 -284
  68. scratchattach/site/activity.py +0 -382
  69. scratchattach/utils/requests.py +0 -93
  70. scratchattach-2.1.15b0.dist-info/RECORD +0 -66
  71. scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
  72. {scratchattach/cloud → cloud}/__init__.py +0 -0
  73. {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
  74. {scratchattach/editor → editor}/code_translation/parse.py +0 -0
  75. {scratchattach/editor → editor}/comment.py +0 -0
  76. {scratchattach/editor → editor}/extension.py +0 -0
  77. {scratchattach/editor → editor}/twconfig.py +0 -0
  78. {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
  79. {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
  80. {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
  81. {scratchattach/other → other}/__init__.py +0 -0
  82. {scratchattach/other → other}/project_json_capabilities.py +0 -0
  83. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
  84. {scratchattach/site → site}/__init__.py +0 -0
  85. {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
  86. {scratchattach/site → site}/browser_cookies.py +0 -0
  87. {scratchattach/utils → utils}/__init__.py +0 -0
cli/__about__.py ADDED
@@ -0,0 +1 @@
1
+ VERSION = "0.0.0"
cli/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """
2
+ Only for use within __main__.py
3
+ """
4
+ import io
5
+
6
+ from rich.console import RenderableType
7
+ from typing_extensions import Optional
8
+ from scratchattach.cli.__about__ import VERSION
9
+ from scratchattach.cli.namespace import ArgSpace
10
+ from scratchattach.cli.context import ctx
11
+
12
+
13
+ # noinspection PyPackageRequirements
14
+ def try_get_img(image: bytes, size: tuple[int, int] | None = None) -> Optional[RenderableType]:
15
+ try:
16
+ from PIL import Image
17
+ from rich_pixels import Pixels
18
+
19
+ with Image.open(io.BytesIO(image)) as image:
20
+ if size is not None:
21
+ image = image.resize(size)
22
+
23
+ return Pixels.from_image(image)
24
+
25
+ except ImportError:
26
+ return ""
cli/cmd/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .login import login
2
+ from .group import group
3
+ from .profile import profile
4
+ from .sessions import sessions
cli/cmd/group.py ADDED
@@ -0,0 +1,127 @@
1
+ from scratchattach.cli import db
2
+ from scratchattach.cli.context import console, ctx
3
+
4
+ from rich.markup import escape
5
+ from rich.table import Table
6
+
7
+ def _list():
8
+ table = Table(title="All groups")
9
+ table.add_column("Name")
10
+ table.add_column("Description")
11
+ table.add_column("Usernames")
12
+
13
+ db.cursor.execute("SELECT NAME, DESCRIPTION FROM GROUPS")
14
+ for name, description in db.cursor.fetchall():
15
+ db.cursor.execute("SELECT USERNAME FROM GROUP_USERS WHERE GROUP_NAME=?", (name,))
16
+ usernames = db.cursor.fetchall()
17
+
18
+ table.add_row(escape(name), escape(description),
19
+ '\n'.join(f"{i}. {u}" for i, (u,) in enumerate(usernames)))
20
+
21
+ console.print(table)
22
+
23
+ def add(group_name: str):
24
+ accounts = input("Add accounts (split by space): ").split()
25
+ for account in accounts:
26
+ ctx.db_add_to_group(group_name, account)
27
+
28
+ def remove(group_name: str):
29
+ accounts = input("Remove accounts (split by space): ").split()
30
+ for account in accounts:
31
+ ctx.db_remove_from_group(group_name, account)
32
+
33
+ def new():
34
+ console.rule(f"New group {escape(ctx.args.group_name)}")
35
+ if ctx.db_group_exists(ctx.args.group_name):
36
+ raise ValueError(f"Group {escape(ctx.args.group_name)} already exists")
37
+
38
+ db.conn.execute("BEGIN")
39
+ db.cursor.execute("INSERT INTO GROUPS (NAME, DESCRIPTION) "
40
+ "VALUES (?, ?)", (ctx.args.group_name, input("Description: ")))
41
+ db.conn.commit()
42
+ add(ctx.args.group_name)
43
+
44
+ _group(ctx.args.group_name)
45
+
46
+ def _group(group_name: str):
47
+ """
48
+ Display information about a group
49
+ """
50
+ db.cursor.execute(
51
+ "SELECT NAME, DESCRIPTION FROM GROUPS WHERE NAME = ?", (group_name,))
52
+ result = db.cursor.fetchone()
53
+ if result is None:
54
+ print("No group selected!!")
55
+ return
56
+
57
+ name, description = result
58
+
59
+ db.cursor.execute("SELECT USERNAME FROM GROUP_USERS WHERE GROUP_NAME = ?", (name,))
60
+ usernames = [name for (name,) in db.cursor.fetchall()]
61
+
62
+ table = Table(title=escape(group_name))
63
+ table.add_column(escape(name))
64
+ table.add_column('Usernames')
65
+
66
+ table.add_row(escape(description),
67
+ '\n'.join(f"{i}. {u}" for i, u in enumerate(usernames)))
68
+
69
+ console.print(table)
70
+
71
+ def switch():
72
+ console.rule(f"Switching to {escape(ctx.args.group_name)}")
73
+ if not ctx.db_group_exists(ctx.args.group_name):
74
+ raise ValueError(f"Group {escape(ctx.args.group_name)} does not exist")
75
+
76
+ ctx.current_group_name = ctx.args.group_name
77
+ _group(ctx.current_group_name)
78
+
79
+ def delete(group_name: str):
80
+ print(f"Deleting {group_name}")
81
+ if not ctx.db_group_exists(group_name):
82
+ raise ValueError(f"Group {group_name} does not exist")
83
+ if ctx.db_group_count == 1:
84
+ raise ValueError(f"Make another group first")
85
+
86
+ ctx.db_group_delete(group_name)
87
+
88
+ def copy(group_name: str, new_name: str):
89
+ print(f"Copying {group_name} as {new_name}")
90
+ if not ctx.db_group_exists(group_name):
91
+ raise ValueError(f"Group {group_name} does not exist")
92
+
93
+ ctx.db_group_copy(group_name, new_name)
94
+
95
+ def rename(group_name: str, new_name: str):
96
+ copy(group_name, new_name)
97
+ delete(group_name)
98
+
99
+ def group():
100
+ match ctx.args.group_command:
101
+ case "list":
102
+ _list()
103
+ case "new":
104
+ new()
105
+ case "switch":
106
+ switch()
107
+ case "add":
108
+ add(ctx.current_group_name)
109
+ case "remove":
110
+ remove(ctx.current_group_name)
111
+ case "delete":
112
+ if input("Are you sure? (y/N): ").lower() != "y":
113
+ return
114
+ delete(ctx.current_group_name)
115
+ new_current = ctx.db_first_group_name
116
+ print(f"Switching to {new_current}")
117
+ ctx.current_group_name = new_current
118
+ _group(new_current)
119
+
120
+ case "copy":
121
+ copy(ctx.current_group_name, ctx.args.group_name)
122
+ case "rename":
123
+ rename(ctx.current_group_name, ctx.args.group_name)
124
+ ctx.current_group_name = ctx.args.group_name
125
+ _group(ctx.args.group_name)
126
+ case None:
127
+ _group(ctx.current_group_name)
cli/cmd/login.py ADDED
@@ -0,0 +1,60 @@
1
+ from scratchattach.cli.context import ctx, console
2
+ from scratchattach.cli import db
3
+ from rich.markup import escape
4
+
5
+ from getpass import getpass
6
+
7
+ import scratchattach as sa
8
+ import warnings
9
+
10
+ warnings.filterwarnings("ignore", category=sa.LoginDataWarning)
11
+
12
+
13
+ def login():
14
+ if ctx.args.sessid:
15
+ if isinstance(ctx.args.sessid, bool):
16
+ ctx.args.sessid = getpass("Session ID: ")
17
+
18
+ session = sa.login_by_id(ctx.args.sessid)
19
+ password = None
20
+ else:
21
+ username = input("Username: ")
22
+ password = getpass()
23
+
24
+ session = sa.login(username, password)
25
+
26
+ console.rule()
27
+ console.print(f"Logged in as [b]{session.username}[/]")
28
+
29
+ # register session
30
+ db.conn.execute("BEGIN")
31
+ db.cursor.execute(
32
+ "INSERT OR REPLACE INTO SESSIONS (ID, USERNAME, PASSWORD) "
33
+ "VALUES (?, ?, ?)", (session.id, session.username, password)
34
+ )
35
+ db.conn.commit()
36
+
37
+ # make new group
38
+ db.cursor.execute("SELECT NAME FROM GROUPS WHERE NAME = ?", (session.username,))
39
+ if not db.cursor.fetchone():
40
+ console.rule(f"Registering [b]{escape(session.username)}[/] as group")
41
+ db.conn.execute("BEGIN")
42
+ db.cursor.execute(
43
+ "INSERT INTO GROUPS (NAME, DESCRIPTION) "
44
+ "VALUES (?, ?)", (session.username, input(f"Description for {session.username}: "))
45
+ ).execute(
46
+ "INSERT INTO GROUP_USERS (GROUP_NAME, USERNAME) "
47
+ "VALUES (?, ?)", (session.username, session.username)
48
+ )
49
+
50
+ db.conn.commit()
51
+
52
+ console.rule()
53
+ if input("Add to current session group? (Y/n)").lower() not in ("y", ''):
54
+ return
55
+
56
+ db.conn.execute("BEGIN")
57
+ db.cursor.execute("INSERT INTO GROUP_USERS (GROUP_NAME, USERNAME) "
58
+ "VALUES (?, ?)", (ctx.current_group_name, session.username))
59
+ db.conn.commit()
60
+ console.print(f"Added to [b]{escape(ctx.current_group_name)}[/]")
cli/cmd/profile.py ADDED
@@ -0,0 +1,7 @@
1
+ from scratchattach.cli.context import ctx, console
2
+
3
+ @ctx.sessionable
4
+ def profile():
5
+ console.rule()
6
+ user = ctx.session.connect_linked_user()
7
+ console.print(user)
cli/cmd/sessions.py ADDED
@@ -0,0 +1,5 @@
1
+ from scratchattach.cli.context import ctx, console
2
+
3
+ @ctx.sessionable
4
+ def sessions():
5
+ console.print(ctx.session)
cli/context.py ADDED
@@ -0,0 +1,142 @@
1
+ """
2
+ Handles data like current session for 'sessionable' commands.
3
+ Holds objects that should be available for the whole CLI system
4
+ Also provides wrappers for some SQL info.
5
+ """
6
+ import argparse
7
+ from dataclasses import dataclass, field
8
+
9
+ import rich.console
10
+ from typing_extensions import Optional
11
+
12
+ from scratchattach.cli.namespace import ArgSpace
13
+ from scratchattach.cli import db
14
+ import scratchattach as sa
15
+
16
+
17
+ @dataclass
18
+ class _Ctx:
19
+ args: ArgSpace = field(default_factory=ArgSpace)
20
+ parser: argparse.ArgumentParser = field(default_factory=argparse.ArgumentParser)
21
+ _username: Optional[str] = None
22
+ _session: Optional[sa.Session] = None
23
+
24
+ # TODO: implement this
25
+ def sessionable(self, func):
26
+ """
27
+ Decorate a command that will be run for every session in the group.
28
+ """
29
+
30
+ def wrapper(*args, **kwargs):
31
+ for username in self.db_users_in_group(self.current_group_name):
32
+ self._username = username
33
+ self._session = None
34
+ func(*args, **kwargs)
35
+
36
+ return wrapper
37
+
38
+ @property
39
+ def username(self):
40
+ if not self._username:
41
+ self._username = self.db_users_in_group(self.current_group_name)[0]
42
+
43
+ return self._username
44
+
45
+ @property
46
+ def session(self):
47
+ if not self._session:
48
+ self._session = sa.login_by_id(self.db_get_sessid(self.username))
49
+
50
+ return self._session
51
+
52
+ # helper functions with DB
53
+ # if possible, put all db funcs here
54
+
55
+ @property
56
+ def current_group_name(self):
57
+ return db.cursor \
58
+ .execute("SELECT * FROM CURRENT WHERE GROUP_NAME IS NOT NULL") \
59
+ .fetchone()[0]
60
+
61
+ @current_group_name.setter
62
+ def current_group_name(self, value: str):
63
+ db.conn.execute("BEGIN")
64
+ db.cursor.execute("DELETE FROM CURRENT WHERE GROUP_NAME IS NOT NULL")
65
+ db.cursor.execute("INSERT INTO CURRENT (GROUP_NAME) VALUES (?)", (value,))
66
+ db.conn.commit()
67
+
68
+ @staticmethod
69
+ def db_group_exists(name: str) -> bool:
70
+ return db.cursor.execute("SELECT NAME FROM GROUPS WHERE NAME = ?", (name,)).fetchone() is not None
71
+
72
+ @staticmethod
73
+ def db_session_exists(name: str) -> bool:
74
+ return db.cursor.execute("SELECT USERNAME FROM SESSIONS WHERE USERNAME = ?", (name,)).fetchone() is not None
75
+
76
+ @staticmethod
77
+ def db_users_in_group(name: str) -> list[str]:
78
+ return [i for (i,) in db.cursor.execute(
79
+ "SELECT USERNAME FROM GROUP_USERS WHERE GROUP_NAME = ?", (name,)).fetchall()]
80
+
81
+ def db_remove_from_group(self, group_name: str, username: str):
82
+ if username in self.db_users_in_group(group_name):
83
+ db.conn.execute("BEGIN")
84
+ db.cursor.execute("DELETE FROM GROUP_USERS "
85
+ "WHERE USERNAME = ? AND GROUP_NAME = ?", (username, group_name))
86
+ db.conn.commit()
87
+
88
+ @staticmethod
89
+ def db_get_sessid(username: str) -> Optional[str]:
90
+ ret = db.cursor.execute("SELECT ID FROM SESSIONS WHERE USERNAME = ?", (username,)).fetchone()
91
+ if ret:
92
+ ret = ret[0]
93
+ return ret
94
+
95
+ def db_add_to_group(self, group_name: str, username: str):
96
+ if username in self.db_users_in_group(group_name) or not self.db_session_exists(username):
97
+ return
98
+ db.conn.execute("BEGIN")
99
+ db.cursor.execute("INSERT INTO GROUP_USERS (GROUP_NAME, USERNAME) "
100
+ "VALUES (?, ?)", (group_name, username))
101
+ db.conn.commit()
102
+
103
+ @staticmethod
104
+ def db_group_delete(group_name: str):
105
+ db.conn.execute("BEGIN")
106
+ # delete links to sessions first
107
+ db.cursor.execute("DELETE FROM GROUP_USERS WHERE GROUP_NAME = ?", (group_name,))
108
+ # delete group itself
109
+ db.cursor.execute("DELETE FROM GROUPS WHERE NAME = ?", (group_name,))
110
+ db.conn.commit()
111
+
112
+ @staticmethod
113
+ def db_group_copy(group_name: str, copy_name: str):
114
+ db.conn.execute("BEGIN")
115
+ # copy group metadata
116
+ db.cursor.execute("INSERT INTO GROUPS (NAME, DESCRIPTION) "
117
+ "SELECT ?, DESCRIPTION FROM GROUPS WHERE NAME = ?", (copy_name, group_name,))
118
+ # copy sessions
119
+ db.cursor.execute("INSERT INTO GROUP_USERS (GROUP_NAME, USERNAME) "
120
+ "SELECT ?, USERNAME FROM GROUP_USERS WHERE GROUP_NAME = ?", (copy_name, group_name,))
121
+
122
+ db.conn.commit()
123
+
124
+ @property
125
+ def db_first_group_name(self) -> str:
126
+ """Just get a group, I don't care which"""
127
+ db.cursor.execute("SELECT NAME FROM GROUPS")
128
+ return db.cursor.fetchone()[0]
129
+
130
+ @property
131
+ def db_group_count(self):
132
+ db.cursor.execute("SELECT COUNT(*) FROM GROUPS")
133
+ return db.cursor.fetchone()[0]
134
+
135
+ def db_get_sess(self, sess_name: str):
136
+ if sess_id := self.db_get_sessid(sess_name):
137
+ return sa.login_by_id(sess_id)
138
+ return None
139
+
140
+
141
+ ctx = _Ctx()
142
+ console = rich.console.Console()
cli/db.py ADDED
@@ -0,0 +1,66 @@
1
+ """
2
+ Basic connections to the scratch.sqlite file
3
+ """
4
+ import sqlite3
5
+ import sys
6
+ import os
7
+
8
+ from pathlib import Path
9
+
10
+ from typing_extensions import LiteralString
11
+
12
+
13
+ def _gen_appdata_folder() -> Path:
14
+ name = "scratchattach"
15
+ match sys.platform:
16
+ case "win32":
17
+ return Path(os.getenv('APPDATA')) / name
18
+ case "linux":
19
+ return Path.home() / f".{name}"
20
+ case plat:
21
+ raise NotImplementedError(f"No 'appdata' folder implemented for {plat}")
22
+
23
+ _path = _gen_appdata_folder()
24
+ _path.mkdir(parents=True, exist_ok=True)
25
+
26
+ conn = sqlite3.connect(_path / "cli.sqlite")
27
+ cursor = conn.cursor()
28
+
29
+ # Init any tables
30
+ def add_col(table: LiteralString, column: LiteralString, _type: LiteralString):
31
+ try:
32
+ # strangely, using `?` here doesn't seem to work. Make sure to use LiteralStrings, not str
33
+ return cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} {_type}")
34
+ except sqlite3.OperationalError as e:
35
+ if "duplicate column name" not in str(e).lower():
36
+ raise
37
+
38
+ # NOTE: IF YOU WANT TO ADD EXTRA KEYS TO A TABLE RETROACTIVELY, USE add_col
39
+ # note: avoide using select * with tuple unpacking/indexing, because if fields are added, things will break.
40
+ conn.execute("BEGIN")
41
+ cursor.executescript("""
42
+ CREATE TABLE IF NOT EXISTS SESSIONS (
43
+ ID TEXT NOT NULL,
44
+ USERNAME TEXT NOT NULL PRIMARY KEY
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS GROUPS (
48
+ NAME TEXT NOT NULL PRIMARY KEY,
49
+ DESCRIPTION TEXT
50
+ -- If you want to add users to a group, you add to the next table
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS GROUP_USERS (
54
+ GROUP_NAME TEXT NOT NULL,
55
+ USERNAME TEXT NOT NULL
56
+ );
57
+
58
+ -- stores info like current group, last project/studio, etc
59
+ CREATE TABLE IF NOT EXISTS CURRENT (
60
+ GROUP_NAME TEXT NOT NULL
61
+ );
62
+ """)
63
+ add_col("SESSIONS", "PASSWORD", "TEXT")
64
+
65
+
66
+ conn.commit()
cli/namespace.py ADDED
@@ -0,0 +1,14 @@
1
+ import argparse
2
+ from typing_extensions import Optional, Literal
3
+
4
+
5
+ class ArgSpace(argparse.Namespace):
6
+ command: Optional[Literal['login', 'group', 'profile', 'sessions']]
7
+ sessid: bool | str
8
+ username: Optional[str]
9
+ studio_id: Optional[str]
10
+ project_id: Optional[str]
11
+ session_name: Optional[str]
12
+
13
+ group_command: Optional[Literal['list', 'new', 'switch', 'add', 'remove', 'delete', 'copy', 'rename']]
14
+ group_name: str