diff --git a/pwm b/pwm
new file mode 100755
index 0000000000000000000000000000000000000000..a015926231c5205901dd0d5f8c946bd69610e4a1
--- /dev/null
+++ b/pwm
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+
+from argparse import Namespace
+import os
+from typing import Dict, List, Optional
+import requests
+import subprocess
+import argparse
+
+BASE_URL: str = 'https://gitedu.hesge.ch/api/v4'
+
+
+def create_group(token: str, name: str, visibility: str = 'private') -> str:
+    """
+    Create gitlab group from name and visibility given. Need valid api token.
+    Return group_id created.
+    """
+    params = {'path': name, 'name': name, 'visibility': visibility}
+    headers = {'PRIVATE-TOKEN': token}
+
+    group = requests.post(BASE_URL + '/groups',
+                          params=params, headers=headers).json()
+    if 'message' in group:
+        print('Error in creating group: %s' % group)
+        exit(1)
+
+    print("Group '" + group['name'] + "' with id '" + str(group['id']) + "' and visibility '" +
+          group['visibility'] + "' available at '" + group['web_url'] + "'")
+    return str(group['id'])
+
+
+def get_project_name(args: Namespace) -> str:
+    """
+    If name is not defined in args, take it from first email.
+    Split at '@' and take first part.
+    """
+    if args.name:
+        return args.name
+    else:
+        return args.emails.split('@')[0]
+
+
+def emails_to_ids(emails: List[str], headers: Dict[str, str]) -> List[str]:
+    """
+    Get students ids from their emails
+    """
+    user_ids = List[str]
+    for email in emails:
+        user_requested = requests.get(
+            BASE_URL + '/users', params={'search': email}, headers=headers).json()
+        if len(user_requested) == 0:
+            print('No user %s found, operation aborted' % email)
+            exit(1)
+        user_ids.append(user_requested[0]['id'])
+    return user_ids
+
+
+def create_repository(token: str, group_id: str, emails: List[str], name: str, import_url: Optional[str], expires_at: Optional[str]):
+    """
+    Create repository in group_id, with members from emails, a name, and
+    optional import_url and expiration date.
+    """
+    headers = {'PRIVATE-TOKEN': token}
+
+    # Create project from name, import_url (if given) and group_id
+    params = {'name': name, 'namespace_id': group_id, 'visibility': 'private'}
+    if import_url:
+        params['import_url'] = import_url
+    project = requests.post(BASE_URL + '/projects',
+                            params=params, headers=headers).json()
+    if 'message' in project:
+        print('Error in creating project: %s' % project)
+        exit(1)
+    print("Project '" + project['name'] + "' at '" +
+          project['web_url'] + "' created")
+
+    # Allow users with developer access level to push and merge on master
+    access_level = 30
+    params = {'name': 'master', 'push_access_level': str(
+        access_level), 'merge_access_level': str(access_level)}
+    requests.post(BASE_URL + '/projects/' +
+                  str(project['id']) + '/protected_branches', params=params, headers=headers).json()
+
+    # Get students ids from their emails
+    user_ids = emails_to_ids(emails, headers)
+
+    # Add each student as project's developer (level 30)
+    for user_id in user_ids:
+        params = {'user_id': user_id, 'access_level': access_level}
+        if expires_at:
+            params['expires_at'] = expires_at
+        new_user = requests.post(BASE_URL + '/projects/' + str(
+            project['id']) + '/members', params=params, headers=headers).json()
+        if 'message' in new_user:
+            print('Error in adding user: %s' % new_user)
+        else:
+            out = ("Adding '" + new_user['name'] + "' (" + new_user['username'] + ") in '"
+                   + project['name'] + "' with access level: " + str(new_user['access_level']))
+            if expires_at:
+                out += ", expires at: " + new_user['expires_at']
+            print(out)
+
+
+def clone_all(token: str, id: str, directory: str, until_date: Optional[str], source: str = 'group'):
+    """
+    Clone all repositories (from a group or "forks of") from id (group or
+    project id) in directory (created in function).
+    """
+    try:
+        os.mkdir(directory)
+    except OSError:
+        print("Creation of the directory '%s' failed, exit\n" % directory)
+        exit(1)
+
+    params = {'simple': 'true', 'per_page': 100}
+    headers = {'PRIVATE-TOKEN': token}
+
+    if source == 'forks':
+        url = BASE_URL + '/projects/' + id + '/forks'
+    else:
+        url = BASE_URL + '/groups/' + id + '/projects'
+
+    repositories = requests.get(url, params=params, headers=headers).json()
+    if 'message' in repositories:
+        print('Error retrieving repositories: ' + repositories['message'])
+        exit(1)
+
+    for repo in repositories:
+        repo_url = BASE_URL + '/projects/' + str(repo['id']) + '/members'
+        members = requests.get(repo_url, headers=headers).json()
+        if 'message' in members:
+            print('Error retrieving members: ' + members['message'])
+            exit(1)
+
+        ssh_url_to_repo = repo['ssh_url_to_repo']
+        web_url = repo['web_url']
+        members_names = ''
+
+        for member in members:
+            if member['access_level'] > 20:  # Access level greater than "Reporter"
+                members_names += member['username'] + ', '
+
+        if source == 'forks':
+            repo_local_name = repo['namespace']['path']
+        else:
+            repo_local_name = repo['path']
+
+        print('Members: ' + members_names)
+        print('Web url: ' + web_url)
+        print('Cloning in "' + directory + '/' + repo_local_name + '"')
+
+        subprocess.run(["git", "clone", "-q", ssh_url_to_repo,
+                        directory + '/' + repo_local_name])
+        if until_date:
+            commit_id = subprocess.check_output([
+                "git", "rev-list", "-n", "1", "--before=\"" + until_date + "\"",
+                "master"], cwd=directory + '/' + repo_local_name).decode('utf-8').rstrip()
+            subprocess.run(
+                ["git", "checkout", "-q", str(commit_id)],
+                cwd=directory + '/' + repo_local_name)
+            print("Checkout at " + str(commit_id) + "\n")
+        else:
+            print()
+
+
+def command_create_group(args):
+    """
+    docstring
+    """
+    if args.visibility:
+        create_group(args.token, args.group_name, args.visibility)
+    else:
+        create_group(args.token, args.group_name)
+
+
+def command_create_repository(args):
+    """
+    docstring
+    """
+    create_repository(args.token, args.group_id, args.emails.split(
+        ','), get_project_name(args), args.import_url, args.expires_at)
+
+
+def command_clone_all(args):
+    """
+    docstring
+    """
+    if args.forks:
+        clone_all(args.token, args.id, args.directory, args.until_date, 'forks')
+    else:
+        clone_all(args.token, args.id, args.directory, args.until_date)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description='Practical Work Manager - \
+        Manage students PW - Create group, projects or clone repositories')
+    subparsers = parser.add_subparsers(metavar='(group | repo | clone)')
+
+    parser_group = subparsers.add_parser('group', help='Create gitlab group')
+    parser_group.add_argument(
+        "token", metavar="TOKEN", help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens")
+    parser_group.add_argument(
+        "group_name", metavar="GROUP_NAME", help="The group name.")
+    parser_group.add_argument(
+        "--visibility", help="Group visibility. By default private.")
+    parser_group.set_defaults(func=command_create_group)
+
+    parser_repo = subparsers.add_parser('repo', help='Create gitlab project')
+    parser_repo.add_argument(
+        "token", metavar="TOKEN", help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens")
+    parser_repo.add_argument(
+        "group_id", metavar="GROUP_ID", help="The group id (int) where to store the created new project.")
+    parser_repo.add_argument(
+        "emails", metavar="EMAILS", help="Emails list of students working in this project, separated by commas (email1,email2).")
+    parser_repo.add_argument(
+        "-n", "--name", help="The project name. If blank, take the first student name (from email) as name.")
+    parser_repo.add_argument("-i", "--import_url",
+                             help="Import the publicly accessible project by URL given here (optional).")
+    parser_repo.add_argument("-x", "--expires_at",
+                             help="Expiration date to kick off students from this project, at 00:00:00. YYYY-MM-DD format (optional).")
+    parser_repo.set_defaults(func=command_create_repository)
+
+    parser_clone = subparsers.add_parser(
+        'clone', help='Clone the repositories locally')
+    group_clone = parser_clone.add_mutually_exclusive_group()
+    group_clone.add_argument("-g", "--group", action="store_true",
+                             help="Clone repositories from a group (with group_id) or forks of a project (with project_id) (default behavior).")
+    group_clone.add_argument("-f", "--forks", action="store_true",
+                             help="Clone forks of a project (with project_id).")
+    parser_clone.add_argument(
+        "token", metavar="TOKEN", help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens")
+    parser_clone.add_argument(
+        "id", metavar="ID", help="The group_id (int) of the projects or the project_id (int) of the forks.")
+    parser_clone.add_argument(
+        "directory", metavar="DIRECTORY", help="Local directory where clone all repositories.")
+    parser_clone.add_argument(
+        "-u", "--until_date", help="Do a git checkout for all repositories at given date, format \"YYYY-MM-DD hh:mm\" (optional).")
+    parser_clone.set_defaults(func=command_clone_all)
+
+    args = parser.parse_args()
+    print(args)
+    args.func(args)
+