Skip to content

Commit

Permalink
Merge pull request #1 from Aadv1k/dev
Browse files Browse the repository at this point in the history
Release v1 -- implement the base for all that will be implemented in the future
  • Loading branch information
Aadv1k authored Jan 30, 2024
2 parents 7287ed9 + 52b88a0 commit 08dee49
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 31 deletions.
100 changes: 100 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

Unofficial api wrapper for [practically](https://www.practically.com) which is an OS used by many schools to publish assignments, report cards and lectures.

- [Quickstart](#quickstart)
- [Guide](#guide)
- [Session](#session)
- [Get User](#get-user)
- [Get Classrooms](#get-classrooms)
- [Get Assignments](#get-assignments)
- [Recipes](#recipes)
- [Get all assignments that are due in the future](get-all-assignments-that-are-due-in-the-future)

## Quickstart

```python
Expand All @@ -13,3 +22,94 @@ p.create_session("your-assigned-username", "your-password")
user = p.get_user()
print(user.first_name, user.last_name)
```

## Guide

```python
prac = Practically(
base_url = "https://teach.practically.com",
session_file = "session.pickle"
)
```

### Session

```python
prac.create_session("username", "password")
```

or, if you are using dotenv you can

```python
prac.create_session_from_env("username_key", "password_key")
```

If a valid `session_id` is not found in the `session_file` it will create a new one. You can check if the current session is invalid using.

```python
prac.is_session_expired_or_invalid()
```

### Get User

```python
user = prac.get_user()
```
- `user.email`
- `user.first_name`
- `user.last_name`

### Get Classrooms

A user can be enrolled in one or more classrooms.

```python
classrooms = prac.get_classrooms()
print(len(classrooms)) # __getitem__ and __len__ can be used
```

A single clasroom looks like so

- `classroom.name`
- `classroom.owner`
- `classroom.id`

### Get Assignments

This returns all the assignments for the user

```python
assignments = prac.get_assignments("123456") // you can get this through classrooms
print(len(assignments)) # __getitem__ and __len__ can be used
```

A single clasroom looks like so

- `assignment.title`: string
- `assignment.start_time`: returns a datetime object
- `assignment.end_time`: returns a datetime object
- `assignment.attached_pdf_url`: return the link to the pdf CDN

## Recipes

### Get all assignments that are due in the future

```python
from practically.practically import Practically

from datetime import datetime

from dotenv import load_dotenv
load_dotenv()

p = Practically()
p.create_session_from_env("USERNAME", "PASSWORD")

classroom_id = p.get_classrooms()[0].id
assignments = [
a.title
for a in p.get_assignments(classroom_id)
if a.end_time > datetime.today()
]

```
49 changes: 18 additions & 31 deletions practically/api/assignments.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
from bs4 import BeautifulSoup
from bs4 import BeautifulSoup, Tag
from urllib.parse import urlparse, parse_qs
import re

from practically.utils import parse_str_as_datestring

pattern = re.compile(r"\s+")


def str_clean(input_str):
return re.sub(pattern, " ", input_str)
from datetime import datetime


class Assignment:
Expand All @@ -22,30 +15,22 @@ def title(self):
)
return title_element.text.strip() if title_element else None

def __parse_date_dirty(self, s):
return datetime.strptime(
re.sub(r"(\s+)|IST (\(.*\))", " ", s[s.find(":") + 1 :].strip()).strip(),
"%d %b %Y %I:%M %p",
)

@property
def start_time(self):
start_time_element = self.soup.select(
"div.col-xl-3:nth-child(3) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(2)"
)
return (
parse_str_as_datestring(
str_clean(start_time_element[0].text[11 + 1 :].strip())
)
if start_time_element
else None
return self.__parse_date_dirty(
self.soup.select("div.mb-0.text-gray-800")[0].text
)

@property
def end_time(self):
end_time_element = self.soup.select(
"div.col-xl-3:nth-child(3) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(3)"
)
return (
parse_str_as_datestring(
str_clean(end_time_element[0].text[9 + 1 :].strip())
)
if end_time_element
else None
return self.__parse_date_dirty(
self.soup.select("div.mb-0.text-gray-800")[1].text
)

def __str__(self):
Expand All @@ -59,8 +44,10 @@ def get_pdf_id_from_url(url):

@property
def attached_pdf_url(self):
url = self.soup.select_one('a[href*="/v1/studentweb/readpdf/"]')["href"]
id = Assignment.get_pdf_id_from_url(url)
url = self.soup.select_one('a[href*="/v1/studentweb/readpdf/"]')
if not url:
return None
id = Assignment.get_pdf_id_from_url(url["href"])
return f"https://teach.practically.com/v1/files/shared/content/{id[:2]}/{id}/{id}.pdf"


Expand All @@ -71,8 +58,8 @@ def __init__(self, html: str):
self.__populate_with()

def __populate_with(self):
for elem in self.soup.select("div.row:nth-child(2)"):
self.items.append(Assignment(str(elem)))
for child in self.soup.select("div > div > div.card.h-100 > div.card-body"):
self.items.append(Assignment(str(child)))

def __getitem__(self, index):
return self.items[index]
Expand Down
23 changes: 23 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
beautifulsoup4==4.12.3
certifi==2023.11.17
cfgv==3.4.0
charset-normalizer==3.3.2
distlib==0.3.8
exceptiongroup==1.2.0
filelock==3.13.1
identify==2.5.33
idna==3.6
iniconfig==2.0.0
nodeenv==1.8.0
packaging==23.2
platformdirs==4.1.0
pluggy==1.4.0
pre-commit==3.6.0
pytest==8.0.0
python-dotenv==1.0.1
PyYAML==6.0.1
requests==2.31.0
soupsieve==2.5
tomli==2.0.1
urllib3==2.1.0
virtualenv==20.25.0

0 comments on commit 08dee49

Please sign in to comment.